mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-25 03:31:07 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd2306377f | ||
|
|
9b52070968 | ||
|
|
f2afd11d6b | ||
|
|
f5e0653138 | ||
|
|
7163264cb1 | ||
|
|
877d829d08 | ||
|
|
8a347fd773 | ||
|
|
5f847ab283 | ||
|
|
eb7694e513 | ||
|
|
48dfa1ff21 | ||
|
|
3577d95744 |
@@ -1,11 +1,7 @@
|
|||||||
|
|
||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on:
|
on: [push, pull_request]
|
||||||
workflow_dispatch:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches: master
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -17,18 +13,18 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
@@ -40,7 +36,5 @@ jobs:
|
|||||||
# - name: Maven single test
|
# - name: Maven single test
|
||||||
# run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false
|
# run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false
|
||||||
- name: Build with Maven
|
- name: Build with Maven
|
||||||
run: mvn -T 1C clean install -Pdefault
|
run: mvn -T 8 clean test
|
||||||
- name: Test SequencedSet and SequencedMap (requires installed MR-JAR)
|
|
||||||
run: cd tests/test-java16 && mvn test
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,18 +16,18 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
~/.m2
|
~/.m2
|
||||||
key: build-${{ env.cache-name }}
|
key: build-${{ env.cache-name }}
|
||||||
- name: db2
|
- 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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
@@ -37,5 +37,5 @@ jobs:
|
|||||||
- name: Maven version
|
- name: Maven version
|
||||||
run: mvn --version
|
run: mvn --version
|
||||||
- name: H2Database
|
- name: H2Database
|
||||||
run: mvn -T 1C clean package
|
run: mvn -T 8 clean package
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ jobs:
|
|||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: oracle-actions/setup-java@v1
|
uses: oracle-actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
website: jdk.java.net
|
website: jdk.java.net
|
||||||
release: ${{ matrix.java_version }}
|
release: ${{ matrix.java_version }}
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
@@ -37,5 +37,5 @@ jobs:
|
|||||||
- name: Maven version
|
- name: Maven version
|
||||||
run: mvn --version
|
run: mvn --version
|
||||||
- name: Build with Maven
|
- name: Build with Maven
|
||||||
run: mvn test -Pea
|
run: mvn -T 8 test
|
||||||
|
|
||||||
|
|||||||
@@ -16,23 +16,23 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
path:
|
path:
|
||||||
~/.m2
|
~/.m2
|
||||||
key: build-${{ env.cache-name }}
|
key: build-${{ env.cache-name }}
|
||||||
- name: mariadb 10.11
|
- name: mariadb 10.6
|
||||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -16,26 +16,24 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11, 17, 21]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
path:
|
path:
|
||||||
~/.m2
|
~/.m2
|
||||||
key: build-${{ env.cache-name }}
|
key: build-${{ env.cache-name }}
|
||||||
- name: Maven version
|
|
||||||
run: mvn --version
|
|
||||||
- name: Build with Maven
|
- name: Build with Maven
|
||||||
run: mvn package
|
run: mvn package
|
||||||
|
|
||||||
|
|||||||
@@ -16,18 +16,18 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
~/.m2
|
~/.m2
|
||||||
key: build-${{ env.cache-name }}
|
key: build-${{ env.cache-name }}
|
||||||
- name: mysql
|
- 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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
~/.m2
|
~/.m2
|
||||||
key: build-${{ env.cache-name }}
|
key: build-${{ env.cache-name }}
|
||||||
- name: oracle
|
- 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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
~/.m2
|
~/.m2
|
||||||
key: build-${{ env.cache-name }}
|
key: build-${{ env.cache-name }}
|
||||||
- name: postgres
|
- 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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -16,23 +16,23 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
path:
|
path:
|
||||||
~/.m2
|
~/.m2
|
||||||
key: build-${{ env.cache-name }}
|
key: build-${{ env.cache-name }}
|
||||||
- name: sqlserver 2022
|
- name: sqlserver 2017
|
||||||
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-sqlserver.properties
|
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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '10 3 * * 3'
|
- cron: '10 3 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -16,18 +16,18 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
java_version: [21]
|
java_version: [11]
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.java_version }}
|
java-version: ${{ matrix.java_version }}
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Maven cache
|
- name: Maven cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
cache-name: maven-cache
|
cache-name: maven-cache
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ ebean-profiling*.xml
|
|||||||
profiling/
|
profiling/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Local Redis integration test credentials
|
|
||||||
ebean-redis/src/test/resources/redis-local.yml
|
|
||||||
|
|
||||||
# Intellij project files
|
# Intellij project files
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
*.ipr
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
[](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean)
|
[](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/blob/master/LICENSE)
|
||||||
[](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml)
|
[](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml)
|
||||||
[](https://www.graalvm.org/)
|
|
||||||
|
|
||||||
##### Build with database platforms
|
##### Build with database platforms
|
||||||
[](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml)
|
[](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>
|
<tr>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://www.foconis.de/" target="_blank">
|
<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>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
@@ -81,18 +85,6 @@ or [github discussions](https://github.com/ebean-orm/ebean/discussions)
|
|||||||
## Documentation
|
## Documentation
|
||||||
Goto [https://ebean.io/docs/](https://ebean.io/docs/)
|
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
|
||||||
[Maven central - g:io.ebean](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.ebean%22%20)
|
[Maven central - g:io.ebean](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.ebean%22%20)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-clickhouse</artifactId>
|
<artifactId>ebean-platform-clickhouse</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-postgres</artifactId>
|
<artifactId>ebean-platform-postgres</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-db2</artifactId>
|
<artifactId>ebean-platform-db2</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-h2</artifactId>
|
<artifactId>ebean-platform-h2</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-hana</artifactId>
|
<artifactId>ebean-platform-hana</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-mariadb</artifactId>
|
<artifactId>ebean-platform-mariadb</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-mysql</artifactId>
|
<artifactId>ebean-platform-mysql</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</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.3.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.11</postgres.jdbc.version>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ebean</groupId>
|
|
||||||
<artifactId>ebean-api</artifactId>
|
|
||||||
<version>18.3.0</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ebean</groupId>
|
|
||||||
<artifactId>ebean-core</artifactId>
|
|
||||||
<version>18.3.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.3.0</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ebean</groupId>
|
|
||||||
<artifactId>ebean-platform-postgres</artifactId>
|
|
||||||
<version>18.3.0</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ebean</groupId>
|
|
||||||
<artifactId>ebean-net-postgis-types</artifactId>
|
|
||||||
<version>18.3.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>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-nuodb</artifactId>
|
<artifactId>ebean-platform-nuodb</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-oracle</artifactId>
|
<artifactId>ebean-platform-oracle</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</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.3.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.3.0</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ebean</groupId>
|
|
||||||
<artifactId>ebean-core</artifactId>
|
|
||||||
<version>18.3.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.3.0</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ebean</groupId>
|
|
||||||
<artifactId>ebean-platform-postgres</artifactId>
|
|
||||||
<version>18.3.0</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.ebean</groupId>
|
|
||||||
<artifactId>ebean-pgvector-types</artifactId>
|
|
||||||
<version>18.3.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>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<postgis.jdbc.version>2.5.1</postgis.jdbc.version>
|
<postgis.jdbc.version>2.5.1</postgis.jdbc.version>
|
||||||
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
|
<postgres.jdbc.version>42.6.0</postgres.jdbc.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -22,13 +22,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -47,19 +47,19 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-postgres</artifactId>
|
<artifactId>ebean-platform-postgres</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-postgis-types</artifactId>
|
<artifactId>ebean-postgis-types</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-postgres</artifactId>
|
<artifactId>ebean-platform-postgres</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-sqlite</artifactId>
|
<artifactId>ebean-platform-sqlite</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-sqlserver</artifactId>
|
<artifactId>ebean-platform-sqlserver</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,13 +17,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -42,13 +42,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-postgres</artifactId>
|
<artifactId>ebean-platform-postgres</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
<relativePath>../..</relativePath>
|
<relativePath>../..</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
@@ -17,31 +17,31 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-api</artifactId>
|
<artifactId>ebean-api</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-core</artifactId>
|
<artifactId>ebean-core</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-joda-time</artifactId>
|
<artifactId>ebean-joda-time</artifactId>
|
||||||
<version>14.0.0</version>
|
<version>13.18.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-jackson-jsonnode</artifactId>
|
<artifactId>ebean-jackson-jsonnode</artifactId>
|
||||||
<version>14.0.0</version>
|
<version>13.18.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-jackson-mapper</artifactId>
|
<artifactId>ebean-jackson-mapper</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -60,13 +60,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-querybean</artifactId>
|
<artifactId>ebean-querybean</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<artifactId>ebean-platform-all</artifactId>
|
<artifactId>ebean-platform-all</artifactId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|||||||
+1
-3
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>composites</artifactId>
|
<artifactId>composites</artifactId>
|
||||||
@@ -24,8 +24,6 @@
|
|||||||
<module>ebean-oracle</module>
|
<module>ebean-oracle</module>
|
||||||
<module>ebean-postgres</module>
|
<module>ebean-postgres</module>
|
||||||
<module>ebean-postgis</module>
|
<module>ebean-postgis</module>
|
||||||
<module>ebean-net-postgis</module>
|
|
||||||
<module>ebean-pgvector</module>
|
|
||||||
<!-- <module>sqlanywhere</module>-->
|
<!-- <module>sqlanywhere</module>-->
|
||||||
<module>ebean-sqlite</module>
|
<module>ebean-sqlite</module>
|
||||||
<module>ebean-sqlserver</module>
|
<module>ebean-sqlserver</module>
|
||||||
|
|||||||
-252
@@ -1,252 +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()` | Flat DTO projection reads | `new QOrder().asDto(OrderSummary.class).findList();` |
|
|
||||||
| `mapTo(...).findList()` | Nested DTO graph projection reads | `new QCustomer().mapTo(CustomerDto.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) |
|
|
||||||
| Map nested entity graphs to DTO graphs | [mapping-entity-graphs-to-dtos.md](guides/mapping-entity-graphs-to-dtos.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,990 +0,0 @@
|
|||||||
# Nested DTO Mapping — API Design
|
|
||||||
|
|
||||||
Design spike for the accepted requirements in [dto-mapping-requirements.md](./dto-mapping-requirements.md),
|
|
||||||
covering issue #2540. This captures the concrete API shape, annotations, and open-question decisions made
|
|
||||||
during design review — before implementation begins.
|
|
||||||
|
|
||||||
## Two source-vs-target mapping pipelines
|
|
||||||
|
|
||||||
Ebean now has (or will have) two distinct DTO pipelines. It's important callers can tell which one they're
|
|
||||||
using:
|
|
||||||
|
|
||||||
1. **`asDto(Dto.class)`** (existing) — a `DtoQuery`, executed directly against a flat SQL `ResultSet`.
|
|
||||||
One row -> one DTO, via constructor/setter matching. No nested ToOne/ToMany support, no entity graph
|
|
||||||
involved.
|
|
||||||
2. **`mapTo(Dto.class)`** (new) — runs the normal ORM entity query (joins/fetches as usual), producing an
|
|
||||||
*unmodifiable entity graph*, then maps that Java object graph into a DTO graph. Supports nested
|
|
||||||
ToOne/ToMany, identity-aware de-duplication, and derives its own fetch spec from the DTO shape.
|
|
||||||
|
|
||||||
## Proposed API
|
|
||||||
|
|
||||||
```java
|
|
||||||
// Existing flat DtoQuery pipeline — unchanged
|
|
||||||
new QUser().valid.eq(true)
|
|
||||||
.select(firstName, lastName)
|
|
||||||
.asDto(UserInfo.class)
|
|
||||||
.findList();
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
// NEW: nested DTO graph pipeline
|
|
||||||
public class CustomerDto {
|
|
||||||
Long id;
|
|
||||||
String name;
|
|
||||||
AddressDto billingAddress; // ToOne -> nested DTO, matched by property name "billingAddress"
|
|
||||||
List<ContactDto> contacts; // ToMany -> nested DTO list, matched by property name "contacts"
|
|
||||||
|
|
||||||
@DtoPath("billingAddress.line1")
|
|
||||||
String billingLine1; // renamed / flattened path
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ContactDto {
|
|
||||||
Long id;
|
|
||||||
String firstName;
|
|
||||||
String lastName;
|
|
||||||
|
|
||||||
@DtoRef
|
|
||||||
Long customerId; // id-only back-reference, avoids re-embedding CustomerDto (cycle)
|
|
||||||
}
|
|
||||||
|
|
||||||
List<CustomerDto> dtos = new QCustomer()
|
|
||||||
.status.eq(Status.ACTIVE)
|
|
||||||
.mapTo(CustomerDto.class)
|
|
||||||
.findList();
|
|
||||||
```
|
|
||||||
|
|
||||||
Computed values (e.g. `cityOrUnknown` derived from `coalesce(billingAddress.city, 'Unknown')`) are
|
|
||||||
**not** modeled with a `@Formula2` annotation directly on the DTO - that was explored and rejected
|
|
||||||
(see "Formula2-on-DTO scope" below). Instead they're modeled as a plain matching field on the DTO,
|
|
||||||
sourced from an `@Entity @View` entity that itself carries the `@Formula2` - see "Computed/aggregate
|
|
||||||
properties" below for the worked example.
|
|
||||||
|
|
||||||
`mapTo(CustomerDto.class)`:
|
|
||||||
- Introspects `CustomerDto` (recursively, at codegen time) to derive the `select(...)`/`.fetch(...)` spec
|
|
||||||
automatically from the DTO's declared shape.
|
|
||||||
- Forces `setUnmodifiable(true)` under the hood — gives fail-fast + a cheap, non-mutable source graph
|
|
||||||
(satisfies the fail-fast requirement without a separate flag).
|
|
||||||
- Runs the query, then runs a mapper over the resulting entity graph, de-duplicating DTO instances by id
|
|
||||||
for repeated nested references (identity-aware, mirrors the source entity graph's own de-duplication).
|
|
||||||
|
|
||||||
## Decisions made
|
|
||||||
|
|
||||||
### Fetch spec: auto-derived from DTO shape
|
|
||||||
|
|
||||||
The DTO's declared structure (fields, nested DTO types, `@DtoPath` overrides) is the single source of truth
|
|
||||||
for what gets selected/fetched from the database. Callers do not need to separately maintain a `.fetch(...)`
|
|
||||||
spec in parallel with the DTO — this directly addresses the original issue's pain point (DTO and query
|
|
||||||
projection drifting out of sync).
|
|
||||||
|
|
||||||
### Entry point naming: `mapTo(Dto.class)`
|
|
||||||
|
|
||||||
Chosen over `asGraph(...)` / `into(...)` / overloading `findList(Class)`. Reads clearly as "map the
|
|
||||||
resulting entity graph to this DTO type" and is unambiguous against the existing `asDto(...)` (flat,
|
|
||||||
SQL-row-based) mechanism.
|
|
||||||
|
|
||||||
### Cycle handling: codegen-time DAG check + `@DtoRef` escape hatch
|
|
||||||
|
|
||||||
Because the fetch spec and mapper are both derived from the *static* DTO type graph (not live object
|
|
||||||
traversal), cycle detection is a compile-time/codegen-time concern, not a runtime one. This is stronger
|
|
||||||
than the common approach in the ecosystem:
|
|
||||||
|
|
||||||
- **MapStruct** does not auto-detect cycles. It offers an opt-in `@Context` "cycle guard" pattern (an
|
|
||||||
identity map of already-mapped source -> target objects) that the developer must wire up manually to
|
|
||||||
avoid infinite recursion mapping bidirectional object graphs.
|
|
||||||
- **Blaze-Persistence / QueryDSL / JOOQ record mapping** avoid the problem architecturally: view/projection
|
|
||||||
types are required to be a strict tree; a back-reference is modeled as an id or a much shallower type,
|
|
||||||
never the same full view type again.
|
|
||||||
|
|
||||||
Ebean's approach: fail the build at annotation-processing time if a DTO's declared type graph is not a DAG,
|
|
||||||
with a clear error message. Provide `@DtoRef` as an explicit escape hatch for intentional back-references
|
|
||||||
(e.g. `Contact.customer`) — it maps only the id, not the full nested DTO, breaking the cycle by design
|
|
||||||
rather than by runtime guard.
|
|
||||||
|
|
||||||
### `@DtoPath` / `@DtoRef`: parallels for readers coming from MapStruct or Blaze-Persistence
|
|
||||||
|
|
||||||
Neither annotation is a novel concept - both map onto things MapStruct and Blaze-Persistence users will
|
|
||||||
already recognise, which is worth spelling out explicitly so it's easy to "grok fast":
|
|
||||||
|
|
||||||
- **`@DtoPath("billingAddress.line1")` is Ebean's equivalent of MapStruct's dot-path `source` flattening**
|
|
||||||
— e.g. `@Mapping(target = "line1", source = "billingAddress.line1")`. MapStruct auto-generates a
|
|
||||||
null-safe chain of getter calls for a dotted `source`; `@DtoPath` does exactly the same thing, just
|
|
||||||
declared on the DTO field itself rather than on a mapper method parameter list. It is also close to
|
|
||||||
Blaze-Persistence's `@Mapping("billingAddress.line1")` on an `@EntityView` attribute, which is a JPQL
|
|
||||||
path expression evaluated the same way — Blaze's placement (directly on the target view property) is
|
|
||||||
actually the closer analogue of the two, since Ebean's `@DtoPath` is likewise placed on the DTO field.
|
|
||||||
The difference from Blaze: `@DtoPath` is restricted to plain getter-chain navigation (no arbitrary JPQL/
|
|
||||||
SQL expression) - see "Formula2-on-DTO scope" below for the boundary and why full expression support is
|
|
||||||
deliberately deferred.
|
|
||||||
- **`@DtoRef` has no dedicated equivalent in either tool** - both MapStruct and Blaze would express the
|
|
||||||
same "just the id" mapping as a plain dot-path to `.id` (`@Mapping(source = "customer.id")` / Blaze
|
|
||||||
`@Mapping("customer.id")`), with no special marker for it. What `@DtoRef` adds beyond that shorthand is
|
|
||||||
*intent*: it tells the codegen this property is a deliberate cycle-breaking reference, so (a) it adds
|
|
||||||
just the association's own name (not a dotted `.id` path) to the generated fetch spec's root
|
|
||||||
`select(...)` - reading the FK column directly with no join, and skipped entirely if that same
|
|
||||||
association is already fully fetched by a `NESTED_ONE`/`NESTED_MANY` property elsewhere on the same DTO
|
|
||||||
(see `DtoMapperWriter.fetchGroupChainCalls()`'s `case REF` branch) - and (b) it participates in the
|
|
||||||
codegen-time DAG cycle check above as an explicit "this is fine, don't flag it" signal, rather than
|
|
||||||
requiring a suppression escape hatch bolted on afterwards.
|
|
||||||
|
|
||||||
**Bug found and fixed while building the aggregation worked example below:** the original implementation
|
|
||||||
excluded `REF` properties from the fetch spec *entirely*, on the assumption the id is "already available
|
|
||||||
off an unfetched reference without triggering a fetch/lazy load". That assumption is only true when some
|
|
||||||
*other* property on the same DTO happens to also fetch that association (as was always the case in the
|
|
||||||
existing hand-built examples). Tested directly against a bare `@ManyToOne` with no other fetch of it:
|
|
||||||
accessing `.getCustomer().getId()` in that case triggers a full lazy-reload of the owning row (extra SQL,
|
|
||||||
not free) - and for an aggregation query it's worse, since the property being grouped by must be selected
|
|
||||||
or the query can't group correctly at all. Fixed so `REF` always contributes its association name to the
|
|
||||||
root `select(...)` (deduped against any existing `NESTED_ONE`/`NESTED_MANY` fetch of the same path).
|
|
||||||
|
|
||||||
**Bug found and fixed (validation phase, testing against `central-access`): primitive-typed field +
|
|
||||||
nullable intermediate hop = unboxing `NullPointerException`.** A multi-hop `@DtoPath` (or `@DtoRef`,
|
|
||||||
which is always 2-hop) null-guards each intermediate getter with a ternary, e.g.
|
|
||||||
`(source.getOrganisation() == null ? null : source.getOrganisation().getId())`. That ternary's static
|
|
||||||
type is always the boxed wrapper (`Long`), since one branch is the `null` literal - fine when the DTO
|
|
||||||
field is itself a reference type (`Long organisationId`), but when the DTO field is a **primitive**
|
|
||||||
(`long organisationId`), passing that boxed expression to the constructor auto-unboxes it, throwing an
|
|
||||||
unhelpful `NullPointerException` at runtime whenever the relation really is `null`. This compiled clean
|
|
||||||
and only failed at runtime with real (nullable) production data - exactly the kind of gap a hand-written
|
|
||||||
mapper would defensively guard against (e.g. `cEbox.getOrganisation() == null ? 0 : ...getId()`) but
|
|
||||||
generated code didn't.
|
|
||||||
|
|
||||||
Fixed in the generator: when a multi-hop `SCALAR`/`REF` property's DTO field type is primitive, the
|
|
||||||
whole null-guarded chain is now wrapped in a small runtime helper (`io.ebean.DtoMapperSupport`) that
|
|
||||||
resolves it safely:
|
|
||||||
- **Default** (`@DtoPath` with no `failOnNull`, or any `@DtoRef`): silently defaults to the primitive's
|
|
||||||
zero-equivalent value (`0`/`false`/etc.) - matches the old hand-written-mapper convention.
|
|
||||||
- **`@DtoPath(failOnNull = true)`**: throws a clear `IllegalStateException` naming the offending property
|
|
||||||
path instead, for callers who'd rather fail fast than silently mask a null they don't expect.
|
|
||||||
|
|
||||||
`@DtoRef` has no `failOnNull` attribute (it has no other attributes at all) - it always uses the
|
|
||||||
default (silent zero) behaviour. See `PrimitiveNullPathDto`/`PrimitiveNullPathFailOnNullDto` /
|
|
||||||
`TestPrimitiveNullPath` for regression coverage.
|
|
||||||
|
|
||||||
### Read-only entity memory overhead: `InterceptReadOnly`
|
|
||||||
`setUnmodifiable(true)` isn't just a behavioural fail-fast flag - it also swaps the per-bean intercept
|
|
||||||
implementation to `InterceptReadOnly`, which is deliberately minimal: just a `boolean[] loaded` (one flag
|
|
||||||
per property) and a `boolean frozen`, plus the inherited owner reference and `fullyLoadedBean` flag. Compare
|
|
||||||
to `InterceptReadWrite` (the mutable/updatable variant), which additionally carries a `ReentrantLock`, four
|
|
||||||
transient collaborator references (`NodeUsageCollector`, `PersistenceContext`, `BeanLoader`,
|
|
||||||
`PreGetterCallback`), a `byte[] flags` array (per-property loaded+changed+dirty+orig-value-set state),
|
|
||||||
`Object[] origValues`, `Exception[] loadErrors`, `MutableValueInfo[]`/`MutableValueNext[]`, and several more
|
|
||||||
scalar bookkeeping fields. None of that is needed for a bean that will only ever be read, so
|
|
||||||
`setUnmodifiable(true)` graphs carry meaningfully less per-instance overhead than normal fetched entities -
|
|
||||||
relevant here because `mapTo(Dto.class)` forces `setUnmodifiable(true)` on its underlying query, making the
|
|
||||||
*source* graph for a DTO mapping cheaper than the equivalent normal (writable) entity graph would be.
|
|
||||||
|
|
||||||
### Ad-hoc computed/formula properties: model as `@Entity @View`/`@Sql`, not ad-hoc SQL-on-DTO
|
|
||||||
|
|
||||||
The "fully ad-hoc SQL-on-DTO" stretch goal above (closer to Blaze's arbitrary `@Mapping` expressions) doesn't
|
|
||||||
need to be built as a bespoke DTO-annotation-processing feature. Ebean already supports modelling read-only,
|
|
||||||
computed, or view-backed data as ordinary entities via `@Entity` + `@View` (backed by a SQL view, e.g. one
|
|
||||||
with aggregates/computed columns) or `@Entity` + `@Sql` (backed by arbitrary `RawSql`, no base table). Given
|
|
||||||
that, the Blaze-Persistence-style "an entity view attribute backed by an arbitrary SQL expression" need can
|
|
||||||
usually be satisfied by:
|
|
||||||
|
|
||||||
1. Modelling the computed/derived shape as its own `@Entity @View` (or `@Sql`) "read entity" - the SQL
|
|
||||||
expression/aggregation lives in the view definition, not in a new annotation-processed DTO mechanism.
|
|
||||||
`@View`'s `name()` doesn't have to point at a genuinely separate database view - it can just point at
|
|
||||||
an *existing* table (e.g. `@View(name = "contact")` on a second entity class reading the same table as
|
|
||||||
`Contact`) purely to mark the entity as view-like/read-only, in which case Ebean's DDL generator emits
|
|
||||||
**no new table or view at all** for it - it's just a second lens onto the same physical data.
|
|
||||||
2. Mapping *that* entity into a plain DTO using the existing, already-implemented `@DtoMapping` machinery -
|
|
||||||
no ad-hoc-SQL-on-DTO support required, since there's no computed expression left to resolve at the DTO
|
|
||||||
layer at all; it's just another entity-to-DTO mapping.
|
|
||||||
3. This read entity benefits from the same `setUnmodifiable(true)`/`InterceptReadOnly` memory efficiency
|
|
||||||
above when used purely as `mapTo(...)` input, so there's no meaningful cost to preferring this over a
|
|
||||||
hypothetical native ad-hoc-SQL-on-DTO feature.
|
|
||||||
|
|
||||||
This significantly narrows (and may eliminate) the case for a dedicated ad-hoc-SQL-on-DTO mechanism - it
|
|
||||||
remains listed as an open stretch goal below primarily for the case where a computed value's SQL is genuinely
|
|
||||||
one-off/DTO-specific and not worth promoting to a standalone `@View`/`@Sql` entity.
|
|
||||||
|
|
||||||
**Worked example** (`tests/test-dto-mapping`): `ContactSummary` is `@Entity @View(name = "contact")` (no new
|
|
||||||
DDL - reads the same table as `Contact`) with `@Formula2("concat(firstName, ' ', lastName)")` computing
|
|
||||||
`fullName`; `ContactSummaryDto` is a plain two-field DTO; `@DtoMapping(source = ContactSummary.class, target
|
|
||||||
= ContactSummaryDto.class)` generates `ContactSummaryDtoMapper` exactly like any other entity→DTO pair - the
|
|
||||||
formula property is just selected like any other field (`select("id,fullName")` in the generated
|
|
||||||
`fetchGroup()`). See `TestContactSummaryDtoMapping`.
|
|
||||||
|
|
||||||
### Aggregate/group-by computed properties: `@Sum`/`@Aggregation` as `@Entity @View`, same pattern
|
|
||||||
|
|
||||||
Ebean's `@Sum` (shorthand for `@Aggregation("sum($1)")`) and `@Aggregation("count(...)"/"sum(...)"/"avg(...)"/
|
|
||||||
"min(...)"/"max(...)")` are the group-by parallel to the formula pattern above - the same `@Entity @View`
|
|
||||||
approach applies, just with an implicit `GROUP BY` instead of a per-row computed column. Ebean auto-derives
|
|
||||||
the `GROUP BY` clause from whichever non-aggregate properties end up in the query's `select()`/`fetch()` - so
|
|
||||||
a second `@Entity @View(name = <same base table>)` entity with one or more `@Sum`/`@Aggregation` properties
|
|
||||||
plus a `@ManyToOne` grouping key becomes a per-parent rollup, with **no new table/view and no explicit
|
|
||||||
`.groupBy()` call required**. This is Ebean's parallel to Blaze-Persistence entity view correlated aggregate
|
|
||||||
mappings, e.g. `@Mapping("SIZE(contacts)")` / `@Mapping("SUM(contacts.engagementScore)")` on an `@EntityView`.
|
|
||||||
|
|
||||||
**Nuance found while building the worked example, and since fixed: `@DtoRef` originally didn't fit the
|
|
||||||
grouping key.** `@DtoRef` was originally excluded from the generated `select()`/`fetch()` spec entirely, on
|
|
||||||
the premise that the id is already available off an unfetched reference for an ordinary entity graph. That
|
|
||||||
premise doesn't hold for an aggregation query: the `@ManyToOne` *is* the property being grouped by, so if
|
|
||||||
it's never selected, the query has nothing to group by. It turned out the premise didn't fully hold for
|
|
||||||
ordinary entity graphs either - see the `@DtoRef` bug writeup above. Fixed so `@DtoRef` now adds the
|
|
||||||
association's own name to the root `select(...)` (reading the FK column directly, no join) - which both
|
|
||||||
supplies the grouping key here and fixes the general-case gap.
|
|
||||||
|
|
||||||
**Worked example** (`tests/test-dto-mapping`): `ContactStats` is `@Entity @View(name = "contact")` (no new
|
|
||||||
DDL - reads the same table as `Contact`/`ContactSummary`) with `@Aggregation("count(id)") contactCount` and
|
|
||||||
`@Sum Integer engagementScore` (a new nullable field added to `Contact` purely to have something to sum),
|
|
||||||
grouped by its `@ManyToOne customer`. `ContactStatsDto` is a flat 3-field DTO (`customerId`, `contactCount`,
|
|
||||||
`engagementScore`), with `customerId` mapped via plain `@DtoRef`. The generated `ContactStatsDtoMapper`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
this.fetchGroup = FetchGroup.of(ContactStats.class)
|
|
||||||
.select("customer,contactCount,engagementScore")
|
|
||||||
.build();
|
|
||||||
...
|
|
||||||
// skip DtoMapContext, only ever a top-level mapping
|
|
||||||
return new ContactStatsDto(
|
|
||||||
(source.getCustomer() == null ? null : source.getCustomer().getId()),
|
|
||||||
source.getContactCount(),
|
|
||||||
source.getEngagementScore());
|
|
||||||
```
|
|
||||||
|
|
||||||
confirmed (via `LoggedSql`) to produce `select t0.customer_id, count(t0.id), sum(t0.engagement_score) from
|
|
||||||
contact t0 ... group by t0.customer_id` - **no join**, one row per customer, correctly summed and counted.
|
|
||||||
See `TestContactStatsDtoMapping`.
|
|
||||||
|
|
||||||
### Formula2-on-DTO scope (v1): existing entity formulas only
|
|
||||||
|
|
||||||
`@Formula2` on a DTO property in v1 only pulls in a formula **already declared on the source entity** (or
|
|
||||||
a reachable associated entity) — it does not support fully ad-hoc SQL declared directly on the DTO with no
|
|
||||||
matching entity property. Fully ad-hoc SQL-on-DTO (closer to Blaze's arbitrary `@Mapping` expressions) is a
|
|
||||||
separate, larger stretch goal to revisit once the core graph-mapping mechanism is proven.
|
|
||||||
|
|
||||||
**Attempted and rejected for v1.** A narrower version was implemented (`@Formula2(value)` resolved exactly
|
|
||||||
like `@DtoPath` - a dot-path getter chain - plus a codegen-time validation that the resolved entity property
|
|
||||||
is itself `@Formula2`/`@Formula`-annotated) but was rejected: for the common case (a DTO field with the same
|
|
||||||
name as the entity's formula property) it generated **identical code to a plain unannotated field** - the
|
|
||||||
only difference was the validation, which wasn't judged enough distinct value to justify a new annotation
|
|
||||||
surface. Not implemented. The only way `@Formula2`-on-DTO would add real value is the full ad-hoc-SQL
|
|
||||||
capability described above, which remains an open stretch goal.
|
|
||||||
|
|
||||||
### Mapper implementation strategy: codegen, not reflection (native-image constraint)
|
|
||||||
|
|
||||||
Native-image support is a core Ebean requirement, so the entity-graph -> DTO-graph mapper must not rely on
|
|
||||||
runtime reflection or `MethodHandles`. This ruled out an initial reflection-based spike:
|
|
||||||
|
|
||||||
- Ebean's existing flat `DtoQuery` (`DtoMetaConstructor`) already uses `MethodHandles` via
|
|
||||||
`Lookups.getLookup()`, but there is no `reflect-config.json` / native-image reachability metadata shipped
|
|
||||||
for it anywhere in the repo. That existing approach is not a clean precedent to copy for a bigger,
|
|
||||||
native-image-first feature.
|
|
||||||
- Instead, the approach mirrors `querybean-generator`, which already generates real `.java` source for
|
|
||||||
`Q*` query bean types (not reflection) — consistent with the wider avaje-ecosystem convention
|
|
||||||
(avaje-inject / avaje-jsonb are explicitly reflection-free via compile-time codegen).
|
|
||||||
|
|
||||||
**Implementation sequencing:** hand-write the mapper in the exact shape the annotation processor will
|
|
||||||
eventually generate (plain Java, direct getter/constructor/setter calls, zero reflection) for one concrete
|
|
||||||
example first, to validate the mapping algorithm and API shape quickly without ever introducing throwaway
|
|
||||||
reflective code. That hand-written mapper then becomes the target/acceptance-test shape for the
|
|
||||||
`querybean-generator` annotation processor that automates producing it.
|
|
||||||
|
|
||||||
### Codegen target: Java first
|
|
||||||
|
|
||||||
The mapper generation (requirement r2) targets `querybean-generator` (the existing APT module that already
|
|
||||||
generates `Q*` query beans, reusing its `PropertyMeta` / `ProcessingContext` machinery). Kotlin parity via
|
|
||||||
`kotlin-querybean-generator` is deferred to a later phase — not blocking initial delivery.
|
|
||||||
|
|
||||||
### Mapper composition: one mapper per entity/DTO pair, generic `DtoMapper<SOURCE, TARGET>` interface
|
|
||||||
|
|
||||||
Rather than one large mapper inlining every nested DTO type, each entity/DTO pair gets its own small
|
|
||||||
mapper class - mirroring MapStruct's per-type mapper generation. All mappers implement a shared generic
|
|
||||||
interface (prototyped as `org.tests.dtomapping.DtoMapper<SOURCE, TARGET>` in the spike, expected to move to
|
|
||||||
`io.ebean` as a public type once solidified):
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface DtoMapper<SOURCE, TARGET> {
|
|
||||||
TARGET map(SOURCE source);
|
|
||||||
default List<TARGET> mapList(List<SOURCE> source) { ... }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
A parent mapper composes nested mappers via **constructor injection**, not a static singleton:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public final class CustomerDtoMapper implements DtoMapper<Customer, CustomerDto> {
|
|
||||||
private final DtoMapper<Address, AddressDto> addressMapper;
|
|
||||||
|
|
||||||
public CustomerDtoMapper() {
|
|
||||||
this(new AddressDtoMapper());
|
|
||||||
}
|
|
||||||
|
|
||||||
public CustomerDtoMapper(DtoMapper<Address, AddressDto> addressMapper) {
|
|
||||||
this.addressMapper = addressMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CustomerDto map(Customer source) {
|
|
||||||
if (source == null) return null;
|
|
||||||
return new CustomerDto(source.getId(), source.getName(), addressMapper.map(source.getBillingAddress()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Rationale:
|
|
||||||
- **Composability & reuse** - the same nested DTO type (e.g. `AddressDto`) used from multiple parent DTOs
|
|
||||||
reuses one generated mapper class rather than duplicating inline mapping logic.
|
|
||||||
- **Constructor injection over static state** - avoids a global mutable singleton; a no-arg constructor
|
|
||||||
gives the common case (default nested mapper), while an overload accepting the nested mapper explicitly
|
|
||||||
allows substitution (tests, customization) without touching global state.
|
|
||||||
- **Codegen-friendly** - this shape generates naturally: one top-level mapper class per DTO type, each
|
|
||||||
constructor-injecting the mappers for any nested DTO types it references.
|
|
||||||
|
|
||||||
## ToMany collections and identity de-duplication (dto-spike-tomany-identity)
|
|
||||||
|
|
||||||
Extending the spike (`ebean-test/src/test/java/org/tests/dtomapping/`) to a `Customer` with a
|
|
||||||
`List<Contact> contacts` ToMany, where each `Contact` has a `customer` back-reference, surfaced
|
|
||||||
two things worth recording.
|
|
||||||
|
|
||||||
### The `DtoMapper` interface threads a shared context
|
|
||||||
|
|
||||||
`DtoMapper<SOURCE, TARGET>` was extended so that mapping is always done against a `DtoMapContext`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public interface DtoMapper<SOURCE, TARGET> {
|
|
||||||
TARGET map(SOURCE source, DtoMapContext context);
|
|
||||||
|
|
||||||
default TARGET map(SOURCE source) {
|
|
||||||
return map(source, new DtoMapContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
default List<TARGET> mapList(List<SOURCE> source, DtoMapContext context) { ... }
|
|
||||||
|
|
||||||
default List<TARGET> mapList(List<SOURCE> source) {
|
|
||||||
return mapList(source, new DtoMapContext());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`DtoMapContext` is an identity-keyed cache of already-mapped source -> target instances, created
|
|
||||||
once per top-level `mapList(...)`/`map(...)` call and threaded through every nested `map(...)`
|
|
||||||
call. This lets repeated references to the *same* source entity instance - which Ebean's own
|
|
||||||
persistence context already de-duplicates within one query (`contact.getCustomer() == customer`
|
|
||||||
for the enclosing `Customer`, confirmed by an existing test) - map to the *same* target DTO
|
|
||||||
instance, rather than each producing an equal-but-distinct copy. This is what makes the mapped
|
|
||||||
DTO output "graph shaped" rather than "tree of copies shaped", and is required for r1/r3.
|
|
||||||
|
|
||||||
### Bug found and fixed: the cache must be partitioned by target type, not just source identity
|
|
||||||
|
|
||||||
The first cut of `DtoMapContext` was a single `IdentityHashMap<Object, Object>` keyed only by the
|
|
||||||
source instance. This breaks as soon as the *same* source instance legitimately needs to map to
|
|
||||||
*two different target types* within one graph - which happens immediately with a back-reference:
|
|
||||||
|
|
||||||
- The top-level `CustomerDtoMapper` maps a `Customer` -> full `CustomerDto`.
|
|
||||||
- The nested `ContactDtoMapper`, mapping `contact.getCustomer()` (the *same* `Customer` instance,
|
|
||||||
by identity), maps it -> shallow `CustomerRefDto` (the `@DtoRef`-style escape hatch that avoids
|
|
||||||
the `Customer -> Contact -> Customer` cycle).
|
|
||||||
|
|
||||||
With a single un-partitioned identity map, whichever mapper runs first "wins" the cache slot for
|
|
||||||
that `Customer` instance, and the other mapper incorrectly receives the wrong-typed cached result
|
|
||||||
(a `ClassCastException` at best, silently wrong data at worst). This was caught by a failing test
|
|
||||||
during the spike and fixed by partitioning the cache per target type:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public final class DtoMapContext {
|
|
||||||
private final Map<Class<?>, Map<Object, Object>> mappedByType = new HashMap<>();
|
|
||||||
|
|
||||||
public <S, T> T computeIfAbsent(Class<T> targetType, S source, Function<S, T> mappingFunction) {
|
|
||||||
Map<Object, Object> mapped = mappedByType.computeIfAbsent(targetType, t -> new IdentityHashMap<>());
|
|
||||||
// ... existing/create/put ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Each generated mapper passes its own target DTO `Class` as the first argument, so `Customer ->
|
|
||||||
CustomerDto` and `Customer -> CustomerRefDto` are cached independently even though the key
|
|
||||||
(`Customer` instance) is identical. **This is an implementation detail the codegen must get
|
|
||||||
right** - worth flagging explicitly when `dto-codegen-mapper` starts, since it's easy to
|
|
||||||
regress if the generator is written from scratch without this test coverage in front of it.
|
|
||||||
|
|
||||||
### Codegen optimization: skip the `DtoMapContext` cache for types that are never nested elsewhere
|
|
||||||
|
|
||||||
`DtoMapContext.computeIfAbsent` only ever produces a cache *hit* when the exact same source
|
|
||||||
instance is presented to `map()` more than once within one top-level call - which can only happen
|
|
||||||
when the target type is reachable via more than one path in the graph, i.e. it's used as a
|
|
||||||
`NESTED_ONE`/`NESTED_MANY` property by some *other* `@DtoMapping` pair (e.g. `CustomerRefDto`
|
|
||||||
reached from many `Contact`s via a shared `Customer`, or `AddressDto` shared as `billingAddress`
|
|
||||||
across customers). A type that's only ever a top-level `mapTo(...)`/`mapList(...)` entry point can
|
|
||||||
never receive the same source instance twice within one call - Ebean's own query engine already
|
|
||||||
de-duplicates root entity instances - so the cache lookup/insert there is pure overhead with a
|
|
||||||
guaranteed-never-hit `IdentityHashMap`.
|
|
||||||
|
|
||||||
Since all `@DtoMapping` pairs are resolved together at codegen time (`DtoMappingReader.
|
|
||||||
resolveAndValidate()`), it's straightforward to compute this: after cycle exclusion, walk every
|
|
||||||
surviving `DtoBeanMeta`'s properties and mark any `nested()` target as `nestedElsewhere()`. The
|
|
||||||
generated `map()` method then branches per mapper:
|
|
||||||
|
|
||||||
```java
|
|
||||||
// CustomerDto - never nested by another mapper, only a mapTo()/mapList() entry point
|
|
||||||
public CustomerDto map(Customer source, DtoMapContext context) {
|
|
||||||
if (source == null) return null;
|
|
||||||
// DtoMapContext for nested mappers only
|
|
||||||
return new CustomerDto(source.getId(), source.getName(),
|
|
||||||
billingAddressMapper.map(source.getBillingAddress(), context),
|
|
||||||
contactsMapper.mapList(source.getContacts(), context));
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddressDto - nested under CustomerDto.billingAddress, so may be shared across customers
|
|
||||||
public AddressDto map(Address source, DtoMapContext context) {
|
|
||||||
if (source == null) return null;
|
|
||||||
// dedup using DtoMapContext, same Address instance can be reached via more than one path in the graph
|
|
||||||
return context.computeIfAbsent(AddressDto.class, source, s -> new AddressDto(
|
|
||||||
s.getId(), s.getLine1(), s.getCity()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ContactSummaryDto - flat, top-level only, no nested children at all
|
|
||||||
public ContactSummaryDto map(ContactSummary source, DtoMapContext context) {
|
|
||||||
if (source == null) return null;
|
|
||||||
// skip DtoMapContext, only ever a top-level mapping
|
|
||||||
return new ContactSummaryDto(source.getId(), source.getFullName());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Deliberately terse, single-line comments - just enough for a developer skimming generated code (e.g.
|
|
||||||
per the earlier `@Formula2`-on-DTO worked example) to know at a glance *why* a given mapper does or
|
|
||||||
doesn't use the cache, without spelling out the full reachability argument inline every time (that
|
|
||||||
lives here in the design doc instead). Note `CustomerDto`'s own construction skips the cache even
|
|
||||||
though it *has* nested children - `context` is still threaded down to `billingAddressMapper`/
|
|
||||||
`contactsMapper` since those target types (`AddressDto`, `ContactDto`) *are* nested elsewhere and
|
|
||||||
still need the identity cache for themselves - hence the distinct "for nested mappers only" wording
|
|
||||||
from the "only ever a top-level mapping" case (`ContactSummaryDto`), which has no children to thread
|
|
||||||
a context to at all.
|
|
||||||
|
|
||||||
### Fetching a ToOne back-reference used only for its FK/id needs the FK property fetched too
|
|
||||||
|
|
||||||
Confirmed (via a first-cut test failure) that if a ToMany's element type has a ToOne back to its
|
|
||||||
parent (e.g. `Contact.customer`), that FK property must itself be included in the fetch
|
|
||||||
(`.fetch("contacts", "id,firstName,lastName,customer")`) even when the mapper only reads the id
|
|
||||||
off the reference. Omitting it throws `LazyInitialisationException: Property not loaded:
|
|
||||||
customer` on the `getCustomer()` call itself (not merely on a property access on the returned
|
|
||||||
reference) - i.e. the earlier "ToOne reference access alone doesn't lazy load" finding
|
|
||||||
(dto-validate-fetch-pagination) only holds once the ToOne/FK property is itself part of the
|
|
||||||
fetch/select spec. This reinforces r6 (auto-deriving the fetch spec from DTO shape): the codegen
|
|
||||||
must include a ToOne property in the fetch spec whenever a DTO needing it (even just its id) is
|
|
||||||
reachable through a ToMany, not just at the top level.
|
|
||||||
|
|
||||||
### Test coverage added
|
|
||||||
|
|
||||||
- `TestCustomerDtoGraphMapping` extended to cover `contacts` ToMany mapping and to assert that
|
|
||||||
sibling `ContactDto`s under the same customer share the identical `CustomerRefDto` instance.
|
|
||||||
- `TestContactDtoGraphMapping` (new) - standalone `ContactDtoMapper` test focused specifically on
|
|
||||||
the identity de-dup guarantee and null-source handling.
|
|
||||||
|
|
||||||
## Codegen foundation: avaje-prisms adopted in querybean-generator (dto-codegen-mapper, step 1)
|
|
||||||
|
|
||||||
Before writing the DTO-mapper annotation-processing logic itself, adopted `avaje-prisms`
|
|
||||||
(`io.avaje:avaje-prisms`) in `querybean-generator` as the mechanism for reading the new
|
|
||||||
`@DtoPath`/`@DtoRef` annotations at APT time, replacing what would otherwise be more hand-rolled
|
|
||||||
`AnnotationMirror` walking (the existing pattern in `FindDbName.java`/`ReadModuleInfo.java`,
|
|
||||||
left as-is/unmigrated - only the *new* annotations use prisms).
|
|
||||||
|
|
||||||
This mirrors the proven pattern already used in two sibling projects in the same ecosystem -
|
|
||||||
`avaje-inject`'s `inject-generator` and `avaje-jsonb`'s `jsonb-generator` - both declare
|
|
||||||
`@GeneratePrism(SomeAnnotation.class)` once and get a generated `SomeAnnotationPrism` with
|
|
||||||
`isPresent(element)` / `getInstanceOn(element)` / `getOptionalOn(element)` and typed accessors
|
|
||||||
for every annotation member (correctly handling `Class`-valued members, avoiding the classic
|
|
||||||
`MirroredTypeException` dance).
|
|
||||||
|
|
||||||
Key property preserved: `querybean-generator` has **zero runtime/compile dependencies today**
|
|
||||||
(confirmed via `mvn dependency:list` returning "none"), matching annotations by FQN string
|
|
||||||
constants (`Constants.java`) rather than importing the actual annotation classes - deliberately
|
|
||||||
keeping the processor free of any dependency footprint for consumers. Adding `avaje-prisms` (to
|
|
||||||
generate the prism wrapper) and `ebean-annotation` (to reference `@DtoPath`/`@DtoRef` as literal
|
|
||||||
`Class` values in `@GeneratePrism(...)`) as `optional` dependencies preserves this: `mvn
|
|
||||||
dependency:list -DincludeScope=runtime` confirms every one of these (plus their own transitive
|
|
||||||
deps: `avaje-prism-core`, `avaje-spi-service`, `avaje-spi-core`) is marked `(optional)`, so none
|
|
||||||
of it propagates to a project that depends on `querybean-generator` (whether as a normal
|
|
||||||
dependency or via `annotationProcessorPaths`).
|
|
||||||
|
|
||||||
New annotations were added to the separate `ebean-annotation` repo (`io.ebean.annotation`
|
|
||||||
package, alongside `@Formula2`), not this repo:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@DtoPath("billingAddress.line1")
|
|
||||||
String billingLine1; // rename/flatten a DTO property from a nested source path
|
|
||||||
|
|
||||||
@DtoRef
|
|
||||||
Integer customerId; // id-only back-reference, breaks what would otherwise be a graph cycle
|
|
||||||
```
|
|
||||||
|
|
||||||
Both use `@Target({FIELD, METHOD})` and `RetentionPolicy.CLASS` - visible to the annotation
|
|
||||||
processor (including across module boundaries, since `CLASS` retention survives in the compiled
|
|
||||||
`.class` file) but absent from runtime reflection, consistent with DTOs remaining plain,
|
|
||||||
framework-free types with no runtime footprint.
|
|
||||||
|
|
||||||
Wiring changes in `querybean-generator`:
|
|
||||||
- `pom.xml`: added `avaje-prisms` (`optional`, plus `annotationProcessorPaths` entry) and
|
|
||||||
`ebean-annotation` (`optional`) dependencies; removed the previous `-proc:none` compiler arg
|
|
||||||
(which would have suppressed `avaje-prisms`' own processor from running to generate the prism
|
|
||||||
source) - annotation processing is now scoped to exactly `avaje-prisms` via the explicit
|
|
||||||
`annotationProcessorPaths` list, so no other processor is auto-discovered.
|
|
||||||
- `module-info.java`: added `requires static io.avaje.prism;` and `requires static
|
|
||||||
io.ebean.annotation;` (`static` = compile-time only, matching the `optional` Maven scope).
|
|
||||||
- New `package-info.java` declaring `@GeneratePrism(DtoPath.class)` and
|
|
||||||
`@GeneratePrism(DtoRef.class)`, generating `DtoPathPrism`/`DtoRefPrism` into
|
|
||||||
`target/generated-sources/annotations`.
|
|
||||||
|
|
||||||
Verified: full `querybean-generator` build + existing test suite pass unchanged, and a downstream
|
|
||||||
full rebuild (`ebean-test` with `-am`) - which exercises the existing Q-bean codegen - also
|
|
||||||
passes with no regressions.
|
|
||||||
|
|
||||||
### Trigger mechanism: `@DtoMapping(source, target)` on a neutral package-info.java
|
|
||||||
|
|
||||||
Considered and rejected: putting a `source`/entity-referencing annotation directly on the DTO
|
|
||||||
class itself (e.g. `@Dto(Customer.class)` on `CustomerDto`). Rejected because DTO types are
|
|
||||||
often owned/generated elsewhere (e.g. from an OpenAPI spec) and must not be forced to reference
|
|
||||||
an internal persistence/entity type - that would leak internal domain types into a
|
|
||||||
public-facing/generated DTO module.
|
|
||||||
|
|
||||||
Instead, adopted the same pattern `avaje-jsonb` uses for external/foreign types it doesn't own
|
|
||||||
(`@Json.Import`): a repeatable annotation declared on a *neutral* holder - a `package-info.java`
|
|
||||||
- naming the `source` entity and `target` DTO as a pair:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@DtoMapping(source = Customer.class, target = CustomerDto.class)
|
|
||||||
@DtoMapping(source = Contact.class, target = ContactDto.class)
|
|
||||||
package org.example.dto;
|
|
||||||
```
|
|
||||||
|
|
||||||
`@DtoMapping` (new, in `ebean-annotation`) is `@Target({PACKAGE, MODULE})`,
|
|
||||||
`@Retention(SOURCE)` (pure codegen trigger, never needed at runtime - unlike `@DtoPath`/
|
|
||||||
`@DtoRef` which need `CLASS` retention to remain visible to the DTO field itself),
|
|
||||||
`@Repeatable(DtoMapping.List.class)` following Java's own repeatable-annotation idiom. Neither
|
|
||||||
the entity nor the DTO needs any annotation of its own.
|
|
||||||
|
|
||||||
**Generated mapper package placement** - also modeled directly on `avaje-jsonb`'s handling of
|
|
||||||
`@Json.Import` for external types (`AdapterName`/`ProcessingContext.isImported`): defaults to the
|
|
||||||
target DTO's own package, *unless* the source or target type belongs to a different Java module
|
|
||||||
than the one being processed, in which case the generated mapper is placed in a package derived
|
|
||||||
from the processing module's own name instead - avoiding a JPMS "split package" violation that
|
|
||||||
would occur from generating source into a package owned by another module. An explicit
|
|
||||||
`mapperPackage` attribute is available to override this for edge cases. Same-module (or
|
|
||||||
non-modular/unnamed-module) projects are unaffected and just get the mapper alongside the DTO.
|
|
||||||
|
|
||||||
### mapTo(Class) dispatch: Class-token API + generated compile-time-safe registry
|
|
||||||
|
|
||||||
The original API sketch above (`mapTo(CustomerDto.class)`) predates the native-image/no-reflection
|
|
||||||
decision. Rather than switching to an instance-based API (`mapTo(new CustomerDtoMapper())`),
|
|
||||||
decided to keep the `Class`-token shape and generate a compile-time-safe registry to resolve it -
|
|
||||||
no reflection, no `Class.forName`, just literal `Class` comparisons generated at build time, e.g.:
|
|
||||||
|
|
||||||
```java
|
|
||||||
<S, D> DtoMapper<S, D> mapperFor(Class<S> sourceType, Class<D> targetType) {
|
|
||||||
if (sourceType == Customer.class && targetType == CustomerDto.class) {
|
|
||||||
return (DtoMapper<S, D>) new CustomerDtoMapper();
|
|
||||||
}
|
|
||||||
if (sourceType == Contact.class && targetType == ContactDto.class) {
|
|
||||||
return (DtoMapper<S, D>) new ContactDtoMapper();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Dispatch is keyed on the **(source, target) pair**, not target alone - this matches how
|
|
||||||
`@DtoMapping(source, target)` pairs are declared, allows the same DTO type to be mapped from more
|
|
||||||
than one source entity without ambiguity, and lets `query.mapTo(dtoType)` fail fast with a clear
|
|
||||||
`PersistenceException` (rather than an incorrect match) when `query.getBeanType()` doesn't pair
|
|
||||||
with the requested DTO.
|
|
||||||
|
|
||||||
This mirrors the existing, already-proven `EbeanEntityRegister`/`EntityClassRegister` mechanism
|
|
||||||
(`SimpleModuleInfoWriter.java`) that `querybean-generator` already generates per module for entity
|
|
||||||
classes - a `List<Class<?>>` built from literal `SomeEntity.class` references, registered via
|
|
||||||
`META-INF/services` (`ServiceLoader`, itself native-image-friendly with no extra reflection
|
|
||||||
config needed for simple no-arg-constructor implementations). The DTO mapper registry follows the
|
|
||||||
same per-module aggregation + `META-INF/services` registration shape, giving `mapTo(Class)` a
|
|
||||||
concrete generated implementation to dispatch through at runtime without reflection anywhere in
|
|
||||||
the chain.
|
|
||||||
|
|
||||||
### mapTo(Dto.class) runtime wiring (implemented)
|
|
||||||
|
|
||||||
`query.mapTo(dtoType)` returns a `MappedQuery<D>` (`findList()`/`findOne()`/`findOneOrEmpty()`/
|
|
||||||
`findStream()`/`findPagedList()`/`usingMaster(boolean)`/`usingTransaction(Transaction)`/`usingConnection(Connection)`).
|
|
||||||
On first use it resolves the generated `DtoMapper<S, D>` for the query's `(getBeanType(), dtoType)`
|
|
||||||
pair via a `DtoMapperManager` (a `ServiceLoader`-backed aggregator over all generated
|
|
||||||
`DtoMapperRegister`s, analogous to `DtoBeanManager`), then:
|
|
||||||
|
|
||||||
- applies `mapper.fetchGroup()` to the query via `query.select(fetchGroup)` - the fetch/select spec
|
|
||||||
is entirely derived from the DTO's declared shape, no manual `.select()`/`.fetch()` needed;
|
|
||||||
- forces `query.setUnmodifiable(true)` - the resulting entity graph is read-only input to the
|
|
||||||
mapper, and any DTO property whose source wasn't actually fetched fails fast with
|
|
||||||
`LazyInitialisationException` rather than silently lazy loading or returning `null`;
|
|
||||||
- executes the query and maps the result(s) via `mapper.map(...)`/`mapper.mapList(...)`.
|
|
||||||
|
|
||||||
An unregistered `(source, dtoType)` pair throws a `PersistenceException` with a suggested
|
|
||||||
`@DtoMapping` fix, at first use (i.e. `findList()`/`findOne()`), not at `mapTo(dtoType)` call time.
|
|
||||||
|
|
||||||
`MappedQuery<D>.usingMaster(boolean)`, `.usingTransaction(Transaction)`, and `.usingConnection(Connection)`
|
|
||||||
all delegate directly to the underlying entity query, mirroring `Query`/`QueryBuilder`. This lets a
|
|
||||||
caller retry against the master data source after a read-replica failure by calling
|
|
||||||
`usingMaster(true)` on the *same* `MappedQuery` instance and re-invoking a find method - there's no
|
|
||||||
need to rebuild the query and call `.mapTo(...)` again.
|
|
||||||
|
|
||||||
`MappedQuery<D>.findStream()` mirrors `QueryBuilder#findStream()` - the underlying entity query is
|
|
||||||
streamed (supporting very large result sets, potentially using multiple persistence contexts
|
|
||||||
internally) and each entity is mapped to its target DTO lazily as the stream is consumed. One
|
|
||||||
`DtoMapContext` is shared across the whole stream (not per-element), so identity de-duplication of
|
|
||||||
nested DTOs (e.g. several `Contact`s sharing the same `Customer`) still holds even when the source
|
|
||||||
entities are never materialized into one `List` at all. As with the entity-level `findStream()`,
|
|
||||||
callers must consume it via try-with-resources to ensure the underlying resources are closed.
|
|
||||||
|
|
||||||
|
|
||||||
## Still open / to revisit during implementation
|
|
||||||
|
|
||||||
- Whether `.fetch(...)` calls can still be layered on top of a `mapTo(Dto.class)` query for explicit
|
|
||||||
overrides. Currently the mapper's `fetchGroup()` is the *only* source of the fetch spec - any
|
|
||||||
`.select()`/`.fetch()` calls made before `.mapTo(...)` are overwritten by it.
|
|
||||||
- Whether `@DtoPath`/`@DtoRef` need additional attributes beyond a bare path/marker (e.g. an explicit
|
|
||||||
target type on `@DtoRef` for disambiguation) once real DTOs with more complex shapes are codegen'd.
|
|
||||||
- Behavior when a DTO property has no matching entity property and no `@DtoPath`/`@Formula2` override
|
|
||||||
(fail at codegen time, most likely, consistent with the "fail fast" philosophy).
|
|
||||||
- `@Formula2`-on-DTO mapping to Blaze-Persistence/QueryDSL-style computed properties - not yet
|
|
||||||
implemented (see requirements doc); a narrower validation-only variant was attempted and rejected
|
|
||||||
as not distinct enough from `@DtoPath` (see "Formula2-on-DTO scope" above). The broader ad-hoc-SQL
|
|
||||||
case likely doesn't need a dedicated DTO feature at all - see "Ad-hoc computed/formula properties"
|
|
||||||
above for the `@Entity @View`/`@Sql` alternative.
|
|
||||||
- **Fetch-path collision between a `NESTED_ONE`/`NESTED_MANY` property and a `@DtoPath` property -
|
|
||||||
found and fixed**: `DtoMapperWriter.fetchGroupChainCalls()` builds one `.fetch(path, ...)`
|
|
||||||
chain-call per distinct fetch path, but the underlying `OrmQueryDetail.fetch(...)` unconditionally
|
|
||||||
**overwrites** (rather than merges) any existing entry for the same path key. If a DTO declared a
|
|
||||||
`NESTED_ONE`/`NESTED_MANY` property AND a `@DtoPath` property whose fetch-path prefix is the *exact
|
|
||||||
same* path (e.g. a nested `AddressDto billingAddress` alongside `@DtoPath("billingAddress.line1")`
|
|
||||||
on the same DTO - both resolve to fetch path `"billingAddress"`), the generator would emit two
|
|
||||||
`.fetch("billingAddress", ...)` calls and the second would silently discard the first's selected
|
|
||||||
properties. Merging wasn't practical - the nested property's `.fetch(path, mapper.fetchGroup())`
|
|
||||||
call passes another mapper's own pre-built, immutable, shared `FetchGroup`, so there's no clean way
|
|
||||||
to splice an extra scalar property into it at the call site. Fixed instead with a **fail-fast
|
|
||||||
compile-time error**: `DtoMapperWriter` now detects the collision and raises a clear
|
|
||||||
`ctx.logError(...)` (annotation-processor `ERROR` diagnostic, fails the compile) naming the
|
|
||||||
colliding property and fetch path, and suggesting the two ways out - move the property onto the
|
|
||||||
nested DTO type instead, or pick a `@DtoPath` that reaches a different, non-colliding path (as
|
|
||||||
`ContactDto.customerCity` already does deliberately, per its own comment, using a 3-segment path).
|
|
||||||
Verified empirically by compiling a small reproduction with a colliding `@DtoPath` and confirming
|
|
||||||
the expected error fires; a permanent regression test
|
|
||||||
(`DtoMapperFetchPathCollisionTest` in `querybean-generator`) now runs this same repro directly
|
|
||||||
through `javax.tools.JavaCompiler` with the `Processor` registered, asserting the compile fails with
|
|
||||||
the expected diagnostic message.
|
|
||||||
- **Compile-time verification of `select(...).asDto(...)` (r6, aspirational) - explored and closed as
|
|
||||||
rejected**: raw SQL is an opaque `String` at compile time, and even the typed query-bean
|
|
||||||
`.select(...)` form only type-checks against the *entity* - the match to the target DTO's constructor
|
|
||||||
still happens at runtime via reflection (`DtoQueryPlanConstructor`), and the `.asDto(...)` call site
|
|
||||||
can be arbitrarily distant from the `.select(...)` call, so there's no fixed AST shape an annotation
|
|
||||||
processor could reliably verify (unlike QueryDSL, whose compile-time safety actually comes from typed
|
|
||||||
`Projections.constructor(...)`/generated Q-type constructor calls, not from checking a select-list
|
|
||||||
against a DTO). `mapTo(Dto.class)` already closes the underlying gap in the tractable direction - it
|
|
||||||
derives the select/fetch spec *from* the DTO's declared shape at APT time, so it is compile-time safe
|
|
||||||
by construction. Recommend `mapTo()` whenever compile-time-checked DTO projection matters, and treat
|
|
||||||
`asDto()`/`findDto()` as the flexible, runtime-checked escape hatch for raw/dynamic SQL. See
|
|
||||||
`dto-mapping-requirements.md` requirement r6.
|
|
||||||
- **Custom property conversion (`@DtoConvert`/`@DtoMixin`, r13/r14) - implemented**: motivated by a
|
|
||||||
real hand-written mapper (`DriverMapper`, central-access) needing both a dependency-free scalar
|
|
||||||
coercion (`short` -> `boolean`) and a dependency-backed conversion (AES decryption via an injected
|
|
||||||
cipher). Final design (see `dto-mapping-requirements.md` section E), as built:
|
|
||||||
- `@DtoConvert(value = ConverterType.class, method = "name")` on a DTO property (combinable with
|
|
||||||
`@DtoPath`); the generator dispatches on whether the referenced method is `static` - static means a
|
|
||||||
direct inlined static call (no registration, covers common reusable coercions), instance means
|
|
||||||
dispatch via a new `DtoConverterManager.get(ConverterType.class).method(...)` call, with the
|
|
||||||
resolved instance wired as a real constructor parameter/field on the generated mapper (same shape
|
|
||||||
as existing nested-mapper constructor injection). Multiple properties on the same mapper sharing
|
|
||||||
the same converter type are deduplicated to a single constructor parameter/field
|
|
||||||
(`DtoBeanMeta.converterDeps()`).
|
|
||||||
- `DtoConverterManager` (`ebean-api`, `io.ebean` package) is a small, narrowly-scoped static put/get
|
|
||||||
bridge - the app registers an already-DI-constructed converter singleton (e.g. built by
|
|
||||||
avaje-inject) *before* building the `Database`. This is a deliberate, narrow exception to the
|
|
||||||
general no-static-mutable-state convention: `ServiceLoader`-discovered, no-arg-constructed
|
|
||||||
generated code (`EbeanDtoMapperRegister`) has no other way to reach an already-DI-constructed
|
|
||||||
singleton. `DtoConverterManager.get(type)` throws immediately if nothing was registered for that
|
|
||||||
type, so a missing converter fails fast at Database-startup time (an eager field initializer on
|
|
||||||
`EbeanDtoMapperRegister`, and equally on each mapper's own no-arg constructor, which resolves the
|
|
||||||
same way via `DtoConverterManager.get(...)` for standalone/test construction), not lazily on first
|
|
||||||
use - `DtoMapperRegister`'s `mapperFor(...)` signature and `DtoMapperManager` are otherwise
|
|
||||||
completely unchanged, as originally planned.
|
|
||||||
- Two alternatives were explored and rejected first: (a) a `DtoMapContext.service(Class)` lookup -
|
|
||||||
wrong lifetime, `DtoMapContext` is a short-lived per-call identity-cache only; (b) a
|
|
||||||
`ServiceLoader`-discovered `DtoConverterSource` SPI mirroring `DtoMapperRegister` itself - can't
|
|
||||||
bridge to an *already* DI-constructed dependency without reconstructing/duplicating it.
|
|
||||||
- `@DtoMixin(Target.class)` - a companion type overlaying `@DtoPath`/`@DtoConvert`/`@DtoRef`
|
|
||||||
annotations onto a DTO that can't be annotated directly (e.g. OpenAPI-generated). Discovered via
|
|
||||||
`roundEnv.getElementsAnnotatedWith(...)` (added to `Processor.getSupportedAnnotationTypes()`,
|
|
||||||
since - unlike `@DtoPath`/`@DtoRef`/`@DtoConvert` - a mixin doesn't annotate an already-iterated
|
|
||||||
field of a known `@DtoMapping` target, so it can't be found lazily). `DtoMappingReader` resolves
|
|
||||||
each target property's annotations from the field itself first, falling back to a same-named
|
|
||||||
method on the registered mixin (`DtoMappingReader.prismOn(...)`) - directly mirrors avaje-jsonb's
|
|
||||||
proven `@Json.MixIn` mechanism.
|
|
||||||
- Implemented in `ebean-annotation` (`DtoConvert`, `DtoMixin`), `ebean-api` (`DtoConverterManager`),
|
|
||||||
and `querybean-generator` (`DtoConverterMeta`, `DtoBeanMeta.converterDeps()`,
|
|
||||||
`DtoMappingReader`/`DtoMapperWriter`/`DtoMapperRegisterWriter` changes). Test coverage:
|
|
||||||
`tests/test-dto-mapping` `TestDtoConvert` (static + instance dispatch, fail-fast unregistered-type
|
|
||||||
check) and `TestDtoMixin` (mixin overlay, including instance-dispatch conversion resolved purely
|
|
||||||
from mixin-declared annotations). The instance-dispatch converter is registered via a
|
|
||||||
`DatabaseConfigProvider` (ServiceLoader hook run before the `Database` is built) rather than a test
|
|
||||||
`@BeforeAll`, since `EbeanDtoMapperRegister`'s mapper fields (including any needing
|
|
||||||
`DtoConverterManager`) are all constructed eagerly during `Database` startup, which can be
|
|
||||||
triggered by whichever test class in the module happens to run first.
|
|
||||||
|
|
||||||
- **Fixed (validation phase, found via `central-access`): `@DtoPath` through a computed/derived
|
|
||||||
getter now fails at compile time, with an explicit `requires()` escape hatch.** `@DtoPath`
|
|
||||||
assumes every dotted segment names a real, fetchable Ebean bean property - so a path like
|
|
||||||
`@DtoPath("currentMachine.organisationMachine.registrationPlate")`, where `getOrganisationMachine()`
|
|
||||||
is a hand-written derived getter (not a real relation/column), used to **compile cleanly** (the
|
|
||||||
codegen had no way to tell it apart from a real property from source alone) but **fail at
|
|
||||||
runtime** with a `PersistenceException: No property found for [organisationMachine] in
|
|
||||||
expression ...`, because the generated `FetchGroup` builder tried to `fetch`/`select` it as if it
|
|
||||||
were a real Ebean property.
|
|
||||||
- Two genuinely separate sub-problems: (1) *detecting* that a path segment isn't a real,
|
|
||||||
fetchable property - solvable at compile time, since a real persistent property always has a
|
|
||||||
backing field (Ebean requires one to enhance), checked via `javax.lang.model`
|
|
||||||
(`ElementFilter.fieldsIn(...)` over the type + superclass chain, see `DtoMappingReader.hasField(...)`);
|
|
||||||
versus (2) *knowing what the computed getter needs fetched* to execute safely - not solvable at
|
|
||||||
compile time without full static/bytecode analysis of the getter's method body, out of scope.
|
|
||||||
- Resolution: don't attempt to infer (2) automatically. When `DtoMappingReader` detects a `@DtoPath`
|
|
||||||
segment with no backing field, it now fails fast at compile time (`ctx.logError(...)`) unless the
|
|
||||||
developer explicitly declares the real entity paths that must be fetched via
|
|
||||||
`@DtoPath(requires = {...})` (dot-notation, same convention as `@DtoPath`'s own `value()`) - e.g.
|
|
||||||
`@DtoPath(value = "primaryContact.lastName", requires = "contacts")` where `getPrimaryContact()`
|
|
||||||
picks the first entry out of the `contacts` collection. The real prefix before the computed
|
|
||||||
segment (if any) is automatically combined with the declared `requires()` paths, so the developer
|
|
||||||
doesn't need to redundantly repeat it. Declared paths are emitted as bare `.fetch(path)` calls in
|
|
||||||
the generated `FetchGroup` (distinct from the `.fetch(path, "props")` shape used for ordinary
|
|
||||||
scalar `@DtoPath` properties, since there's no specific target property list to narrow to here).
|
|
||||||
- **The zero-extra-fetch case is also supported, via an explicit `requires = {}`** - e.g.
|
|
||||||
`@DtoPath(value = "idBadge", requires = {})` where `getIdBadge()` derives purely from `id`
|
|
||||||
(always fetched regardless). An explicit empty array confirms "nothing extra needed", distinct
|
|
||||||
from omitting `requires()` entirely ("not yet considered", still a compile error) - `requires()`
|
|
||||||
itself can't tell the two cases apart (both read back as an empty `List`), so `DtoMappingReader`
|
|
||||||
checks the avaje-prism-generated `DtoPathPrism.values.requires()` instead, which returns `null`
|
|
||||||
only when the member was left at its default (i.e. omitted from source). `DtoPropertyMeta`
|
|
||||||
correspondingly carries `hasComputedSegment()` as its own boolean flag (set whenever a computed
|
|
||||||
segment was detected at all), independent of whether `requiredFetchPaths()` happens to be empty -
|
|
||||||
an earlier version conflated the two (inferring "has a computed segment" from "has a non-empty
|
|
||||||
requiredFetchPaths list"), which broke exactly this explicit-empty case by falling through to the
|
|
||||||
ordinary scalar `.select(...)` path and failing at runtime with `PersistenceException: Property
|
|
||||||
not found - idBadge` (`idBadge` isn't a real Ebean property, so it can't be selected).
|
|
||||||
- Implemented in `ebean-annotation` (`DtoPath.requires()`), and `querybean-generator`
|
|
||||||
(`DtoMappingReader` computed-segment detection/validation, `DtoPropertyMeta.requiredFetchPaths()`/
|
|
||||||
`hasComputedSegment()`, `DtoMapperWriter.fetchGroupChainCalls()` bare-fetch emission). Test
|
|
||||||
coverage: `tests/test-dto-mapping` `ComputedPathDto`/`TestComputedPath` (happy path, `requires`
|
|
||||||
correctly fetches the dependency and the mapped value is correct), `ComputedPathNoFetchDto`/
|
|
||||||
`TestComputedPathNoFetch` (explicit `requires = {}`, genuinely nothing extra needed), and
|
|
||||||
`querybean-generator`'s `DtoMapperComputedPathTest` (negative case - omitting `requires` on a
|
|
||||||
computed segment is a compile-time `ERROR` diagnostic, verified via direct `javax.tools.JavaCompiler`
|
|
||||||
compilation, mirroring `DtoMapperFetchPathCollisionTest`).
|
|
||||||
- Known gap: the dedup between the computed segment's required fetch paths and existing
|
|
||||||
`pathSelect`/`nestedAssocPaths` keys in `DtoMapperWriter` is a simplified exact-path-string check
|
|
||||||
(skip emitting a duplicate `.fetch(path)`), not full collision detection like the existing
|
|
||||||
NESTED_ONE/MANY vs `@DtoPath` check - a bare `fetch(path)` and an existing `fetch(path,
|
|
||||||
"specific,props")` for the same path string are not merged/reconciled, just left as two separate
|
|
||||||
calls if that edge case arises.
|
|
||||||
|
|
||||||
- **Fixed: a single-hop `@DtoPath` rename through a computed/derived getter whose return type is
|
|
||||||
itself a registered nested DTO (`NESTED_ONE`/`NESTED_MANY`, not `SCALAR`) bypassed the
|
|
||||||
computed-segment validation above entirely.** E.g. `@DtoPath("primaryContact")` where the DTO
|
|
||||||
field's declared type is `ContactDto` (a type with its own `@DtoMapping(source = Contact.class,
|
|
||||||
target = ContactDto.class)`) and `getPrimaryContact()` is a computed getter with no backing
|
|
||||||
field on `Customer`. This resolves to a single-segment path, so `DtoMappingReader.resolveProperty()`
|
|
||||||
took its `properties.size() == 1` nested-lookup shortcut and returned early - before the
|
|
||||||
`computedFrom`/`requires()` validation block (added for the `SCALAR` case above) ever ran. The
|
|
||||||
generated `FetchGroup` then emitted a broken `fetch("primaryContact",
|
|
||||||
contactMapper.fetchGroup())` call (`"primaryContact"` isn't a real Ebean fetch path), failing at
|
|
||||||
runtime rather than compile time - the exact class of bug the `SCALAR` fix was meant to close off
|
|
||||||
entirely.
|
|
||||||
- Resolution: restructured `resolveProperty()` so the computed-segment detection/validation block
|
|
||||||
runs *before* the `properties.size() == 1` nested-lookup branch, so both `SCALAR` and
|
|
||||||
`NESTED_ONE`/`NESTED_MANY` paths share the same detection/validation. `DtoPropertyMeta` gained a
|
|
||||||
matching constructor overload for `NESTED_ONE`/`NESTED_MANY` carrying `computedSegment`/
|
|
||||||
`requiredFetchPaths`. In `DtoMapperWriter.fetchGroupChainCalls()`, a `NESTED_ONE`/`NESTED_MANY`
|
|
||||||
property with `hasComputedSegment()` true is routed into `extraFetchPaths` (the same bare
|
|
||||||
`.fetch(path)` mechanism as the `SCALAR` case) instead of emitting `fetch(path,
|
|
||||||
mapper.fetchGroup())` - since the nested mapper's own `FetchGroup` requirements can't be
|
|
||||||
meaningfully attached under a path name that doesn't exist on the source entity.
|
|
||||||
- Note the nested mapper's *own* fetch requirements (e.g. if `ContactDto` itself needed
|
|
||||||
`customer.billingAddress`) are **not** automatically propagated up through a computed segment -
|
|
||||||
only whatever the computed getter itself needs (via `requires()`) is fetched. The nested
|
|
||||||
mapper's `map(...)` call still works via plain Java method invocation regardless (Ebean
|
|
||||||
transparent lazy loading covers any gap), but relying on that silently reintroduces N+1 queries,
|
|
||||||
so the nested DTO used through a computed segment should ideally be a "leaf" shape needing
|
|
||||||
nothing beyond what `requires()` already declares.
|
|
||||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()` restructuring,
|
|
||||||
`DtoPropertyMeta`'s new constructor overload, `DtoMapperWriter.fetchGroupChainCalls()`). Test
|
|
||||||
coverage: `tests/test-dto-mapping` `ContactLeafDto`/`ComputedNestedDto`/`TestComputedNestedPath`
|
|
||||||
(happy path - generated `FetchGroup` is `.select("id").fetch("contacts")`, no broken
|
|
||||||
`fetch("primaryContact", ...)` call, and the mapped value is correct end-to-end), and
|
|
||||||
`querybean-generator`'s `DtoMapperComputedPathTest#dtoPathThroughComputedGetter_targetingNestedDto_withoutRequires_expectCompileError`
|
|
||||||
(negative case, mirroring the `SCALAR` one). The `NESTED_MANY` variant (a computed getter
|
|
||||||
returning a `List` of a type with its own registered nested DTO mapping) shares the identical
|
|
||||||
code path but had no dedicated regression test until later confirmed via `Customer
|
|
||||||
.getRecentContacts()` / `ComputedNestedListDto` / `TestComputedNestedListPath` (coverage only,
|
|
||||||
not a bug fix - passed cleanly first try, confirming the shared code path does work end-to-end
|
|
||||||
for both `NESTED_ONE` and `NESTED_MANY`).
|
|
||||||
|
|
||||||
- **Fixed: `@DtoRef` never checked for a computed/derived association getter at all.** Unlike
|
|
||||||
`@DtoPath`, `@DtoRef`'s association name (derived by stripping the `Id` suffix off the field
|
|
||||||
name, e.g. `primaryContactId` -> `primaryContact`) was never checked against `hasField(...)` -
|
|
||||||
so `@DtoRef` on a computed getter (e.g. `getPrimaryContact()` picking the first entry out of a
|
|
||||||
`contacts` collection) compiled cleanly and generated a broken `FetchGroup.select("primaryContact")`
|
|
||||||
call (`"primaryContact"` isn't a real Ebean property), failing at runtime rather than compile
|
|
||||||
time - the same class of bug as the original `@DtoPath` fix, just entirely unaddressed for
|
|
||||||
`@DtoRef`'s separate code path.
|
|
||||||
- Resolution: `@DtoRef` gained its own `requires()` attribute (dot-notation, same convention and
|
|
||||||
explicit-empty semantics as `@DtoPath#requires()`, using the same `DtoRefPrism.values.requires()
|
|
||||||
== null` omitted-vs-explicit-empty technique). `DtoMappingReader`'s `@DtoRef` branch now checks
|
|
||||||
`hasField(meta.source(), assocName)` and fails fast at compile time (`ctx.logError(...)`) when
|
|
||||||
the association has no backing field and `requires()` wasn't specified. `DtoPropertyMeta`'s
|
|
||||||
`REF` properties now carry `computedSegment`/`requiredFetchPaths` through the existing fields
|
|
||||||
(no new constructor needed - the full constructor already had the right shape).
|
|
||||||
`DtoMapperWriter.fetchGroupChainCalls()`'s `REF` case now checks `hasComputedSegment()` and
|
|
||||||
routes into `extraFetchPaths` (bare `.fetch(path)`) instead of `rootSelect.add(assoc)` when
|
|
||||||
true - the value expression itself (`source.getPrimaryContact().getId()`, null-guarded) is
|
|
||||||
unaffected, since it's plain Java method invocation regardless of whether the association name
|
|
||||||
is a real Ebean property.
|
|
||||||
- Implemented in `ebean-annotation` (`DtoRef.requires()`), and `querybean-generator`
|
|
||||||
(`DtoMappingReader`'s `@DtoRef` branch, `DtoMapperWriter.fetchGroupChainCalls()`'s `REF` case).
|
|
||||||
Test coverage: `tests/test-dto-mapping` `ComputedRefDto`/`TestComputedRefPath` (happy path -
|
|
||||||
generated `FetchGroup` is `.select("id").fetch("contacts")`, no broken `select("primaryContact")`
|
|
||||||
call, and the mapped id is correct end-to-end), and `querybean-generator`'s
|
|
||||||
`DtoMapperComputedPathTest#dtoRefThroughComputedGetter_withoutRequires_expectCompileError`
|
|
||||||
(negative case, mirroring the `@DtoPath` ones).
|
|
||||||
|
|
||||||
- **Fixed: `requires()` path values themselves were never validated against the source type's
|
|
||||||
real property graph.** `@DtoPath(requires = {...})`/`@DtoRef(requires = {...})` values are
|
|
||||||
handed straight through to `FetchGroup.fetch(...)` unmodified - a typo (e.g. `requires =
|
|
||||||
"contactz"` for the real `contacts` property) compiled cleanly, since only the *computed
|
|
||||||
segment itself* was checked against `hasField(...)`, not the developer-declared dependency
|
|
||||||
paths meant to fix it. That silently reintroduced the exact runtime `PersistenceException` the
|
|
||||||
whole `requires()` escape hatch exists to prevent, just one step removed and harder to spot.
|
|
||||||
- Resolution: added `DtoMappingReader.validateRequiresPath(...)`, which walks each dot-notation
|
|
||||||
segment of a declared `requires()` value from the source root (`meta.source()`), checking
|
|
||||||
`hasField(...)` at every hop exactly like `@DtoPath#value()`'s own segments are checked, and
|
|
||||||
unwrapping a `java.util.List`-typed intermediate hop to its element type (via
|
|
||||||
`listElementType(TypeMirror)`) so a collection segment followed by a further hop resolves
|
|
||||||
correctly - needed a new `getterReturnTypeMirror(...)` helper (returning the raw `TypeMirror`
|
|
||||||
rather than converting straight to `TypeElement`, which can't distinguish a `List` from any
|
|
||||||
other declared type) alongside the existing `getterReturnType(...)`. Called for every entry in
|
|
||||||
`pathPrism.requires()`/`refPrism.requires()` right after they're read, for both the `@DtoPath`
|
|
||||||
and `@DtoRef` branches. The already-validated real prefix (segments before the computed one in
|
|
||||||
a `@DtoPath#value()`) is intentionally *not* re-validated, since it was already checked while
|
|
||||||
walking `value()` itself.
|
|
||||||
- Implemented in `querybean-generator` (`DtoMappingReader.validateRequiresPath(...)`,
|
|
||||||
`getterReturnTypeMirror(...)`, called from both the `@DtoPath` and `@DtoRef` branches). Test
|
|
||||||
coverage: `querybean-generator`'s
|
|
||||||
`DtoMapperComputedPathTest#dtoPathRequires_withTypoInPathValue_expectCompileError` (negative
|
|
||||||
case - a typo'd `requires()` segment is a compile-time `ERROR` diagnostic); existing
|
|
||||||
`tests/test-dto-mapping`/`central-access` suites (real multi-segment `requires()` values like
|
|
||||||
`"currentMachine.organisationMachines"`) continue to pass unchanged, confirming the validation
|
|
||||||
doesn't false-positive on legitimate paths.
|
|
||||||
|
|
||||||
- **Fixed: a bare, full `requires()` fetch and a sibling property's narrowed `@DtoPath` fetch of
|
|
||||||
the exact same path silently conflicted, with the narrow one always (incorrectly) winning.**
|
|
||||||
`DtoMapperWriter.fetchGroupChainCalls()`'s dedup logic used to skip emitting a computed
|
|
||||||
segment's bare `fetch(path)` call whenever another property's `@DtoPath` already had a narrowed
|
|
||||||
`fetch(path, "specific,props")` entry for that exact path string - on the assumption the two
|
|
||||||
were interchangeable/redundant. They aren't: `FetchGroup`'s builder (`OrmQueryDetail.fetch(...)`)
|
|
||||||
keys fetch calls by path in a plain `Map` and **replaces** rather than merges same-path entries,
|
|
||||||
so whichever call format was emitted meant the *other* was silently discarded. Since the narrow
|
|
||||||
entry was always emitted first and the bare one skipped whenever it existed, the narrow selection
|
|
||||||
always won - meaning a computed getter's `requires()` declaration could be completely ignored
|
|
||||||
whenever an unrelated sibling `@DtoPath` happened to narrow-select the exact same path, leaving
|
|
||||||
whatever extra properties the computed getter actually touches unfetched (a silent lazy load, or
|
|
||||||
a hard `LazyInitialisationException` outside a persistence context).
|
|
||||||
- Resolution: reversed the priority - `fetchGroupChainCalls()` now skips a narrowed `pathSelect`
|
|
||||||
entry when `extraFetchPaths` (the computed segment's `requires()`) declares the exact same
|
|
||||||
path, letting the bare, full `fetch(path)` call win instead. This is always safe since a full
|
|
||||||
fetch is a superset of any narrower property selection - the narrow entry's own properties are
|
|
||||||
included within it regardless. The existing `nestedAssocPaths` priority (a `NESTED_ONE`/
|
|
||||||
`NESTED_MANY` property's full `fetch(path, mapper.fetchGroup())` always wins over a bare
|
|
||||||
`fetch(path)`) was correct already and left unchanged - a nested mapper's own `FetchGroup` is
|
|
||||||
strictly richer than either form and must not be replaced by either.
|
|
||||||
- Implemented in `querybean-generator` (`DtoMapperWriter.fetchGroupChainCalls()`). Test coverage:
|
|
||||||
`tests/test-dto-mapping` `FetchCollisionDto`/`TestFetchCollisionPath`, plus a new computed
|
|
||||||
getter `Customer.getBillingSummary()` (reads `billingAddress.getLine1()`, deliberately a
|
|
||||||
different `Address` property to the `city` narrowly selected by a sibling `@DtoPath` on the
|
|
||||||
same DTO) - confirmed to reproduce `LazyInitialisationException: Property not loaded: line1`
|
|
||||||
when the fix is reverted, and pass cleanly (correct `line1`-derived value, generated
|
|
||||||
`FetchGroup` is `.select("id").fetch("billingAddress")` with no narrowed variant at all) with
|
|
||||||
it in place.
|
|
||||||
|
|
||||||
- **Fixed: two `@DtoMixin` companion types targeting the same DTO class silently conflicted, with
|
|
||||||
the second-processed one winning.** `DtoMappingReader.collectMixins()` keyed a single
|
|
||||||
`mixinsByTarget` map by the target DTO's FQN, and `Map.put(...)` unconditionally overwrote any
|
|
||||||
existing entry - so if two mixin interfaces (e.g. a legitimate one plus an accidental duplicate,
|
|
||||||
or two independently-added mixins that both happened to target the same generated/unowned DTO)
|
|
||||||
both declared `@DtoMixin(SameDto.class)`, whichever was visited last by
|
|
||||||
`roundEnv.getElementsAnnotatedWith(...)` silently won, and *all* of the other mixin's
|
|
||||||
`@DtoPath`/`@DtoRef`/`@DtoConvert` overlays were discarded with no diagnostic at all.
|
|
||||||
- Resolution: `collectMixins()` now checks for an existing registration before storing a new one
|
|
||||||
and raises a compile `ERROR` naming both the target and the already-registered mixin's
|
|
||||||
qualified name, rather than silently overwriting it.
|
|
||||||
- Implemented in `querybean-generator` (`DtoMappingReader.collectMixins()`). Test coverage: new
|
|
||||||
negative compile-error test `DtoMapperComputedPathTest#duplicateDtoMixin_forSameTarget_expectCompileError`
|
|
||||||
(two minimal `@DtoMixin(FooDto.class)` interfaces both declaring a `bar()` method, compiled
|
|
||||||
together, asserting the `Duplicate @DtoMixin` diagnostic is raised); existing
|
|
||||||
`tests/test-dto-mapping` `TestDtoMixin` (single, legitimate mixin usage) continues to pass
|
|
||||||
unchanged.
|
|
||||||
|
|
||||||
- **Fixed: `@DtoRef` and `@DtoPath` both present on the same field silently conflicted, with
|
|
||||||
`@DtoRef` always (invisibly) winning.** `resolveProperty()` checked `refPrism != null` first and
|
|
||||||
returned immediately whenever present, so a field carrying both annotations at once - whether by
|
|
||||||
copy/paste mistake, a half-finished rename from one style to the other, or simple confusion
|
|
||||||
between the two escape hatches - had its `@DtoPath` completely ignored with no diagnostic at all.
|
|
||||||
- Resolution: `resolveProperty()` now resolves both prisms upfront and raises a compile `ERROR`
|
|
||||||
naming the field when both are present, rather than silently picking `@DtoRef` and discarding
|
|
||||||
`@DtoPath`.
|
|
||||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()`). Test coverage: new
|
|
||||||
negative compile-error test `DtoMapperComputedPathTest#dtoRefAndDtoPath_onSameField_expectCompileError`
|
|
||||||
(a field carrying both `@DtoRef` and `@DtoPath("bar.id")` over a real, non-computed association,
|
|
||||||
isolating the conflict diagnostic from the separate computed-getter `requires()` diagnostics).
|
|
||||||
|
|
||||||
- **Fixed: `@DtoConvert` on a `NESTED_ONE`/`NESTED_MANY` property was silently ignored.**
|
|
||||||
`resolveProperty()` resolves the property's `DtoConverterMeta` unconditionally up front (before
|
|
||||||
it's known whether the property will resolve to `SCALAR`/`REF`/`NESTED_ONE`/`NESTED_MANY`), but
|
|
||||||
only the `SCALAR`/`REF` `DtoPropertyMeta` constructors actually accept/store a converter - the
|
|
||||||
`NESTED_ONE`/`NESTED_MANY` constructor calls never took one, so a resolved converter was simply
|
|
||||||
dropped on the floor with no diagnostic. A developer adding `@DtoConvert` to a nested-DTO field
|
|
||||||
(e.g. hoping to post-process the nested mapper's result) would see it silently do nothing -
|
|
||||||
`DtoMapperWriter.propertyValueExpression()`'s `NESTED_ONE`/`NESTED_MANY` cases call straight into
|
|
||||||
`mapperFieldName(property) + ".map(...)"`/`".mapList(...)"` with no converter wrapping at all.
|
|
||||||
- Resolution: added `rejectConverterOnNested(...)`, called at each of the four call sites that
|
|
||||||
construct a `NESTED_ONE`/`NESTED_MANY` `DtoPropertyMeta` (the single-hop `@DtoPath`-rename
|
|
||||||
branch's two cases, and the plain non-`@DtoPath` branch's two cases) - raises a compile `ERROR`
|
|
||||||
naming the field whenever a converter was resolved for it, rather than silently discarding it.
|
|
||||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveProperty()`,
|
|
||||||
`rejectConverterOnNested()`). Test coverage: new negative compile-error test
|
|
||||||
`DtoMapperComputedPathTest#dtoConvertOnNestedOne_expectCompileError` (a `NESTED_ONE` field
|
|
||||||
carrying `@DtoConvert` over a legitimately nested, separately-`@DtoMapping`-registered type);
|
|
||||||
existing `tests/test-dto-mapping` suite (no nested property currently combines `@DtoConvert`
|
|
||||||
with `NESTED_ONE`/`NESTED_MANY`) continues to pass unchanged, confirming no false positives on
|
|
||||||
plain nested properties.
|
|
||||||
|
|
||||||
- **Fixed: `@DtoConvert(method = ...)` resolution ignored parameter arity/overloads.** The shared
|
|
||||||
`findMethod(type, name)` helper (also used for the builder's `build()` lookup and `@DtoMixin`
|
|
||||||
companion-method lookup) matches purely by simple name - the first `ExecutableElement` found -
|
|
||||||
with no arity or parameter-type check at all. For `@DtoConvert` specifically this is a real risk:
|
|
||||||
its documented contract is a method "taking the source property value and returning the
|
|
||||||
converted DTO property value" (i.e. exactly one parameter), but a shared/reusable conversion
|
|
||||||
utility class is a very plausible place to have multiple same-named overloads (e.g. `format
|
|
||||||
(Instant)` and `format(LocalDate)`) - `findMethod` would silently bind to whichever one
|
|
||||||
`ElementFilter.methodsIn` happened to return first, independent of which one the developer
|
|
||||||
actually meant, generating either a confusing arity/type-mismatch compile error in the generated
|
|
||||||
mapper or, if both overloads happened to be call-compatible, silently invoking the wrong one.
|
|
||||||
- Resolution: added a dedicated `findConverterMethod(...)` (used only by `resolveConverter()`,
|
|
||||||
leaving the shared `findMethod()` untouched for the builder/mixin call sites which have their
|
|
||||||
own, different arity expectations) that filters same-named candidates down to those taking
|
|
||||||
exactly one parameter. Zero matches raises a clear "not found ... taking exactly one
|
|
||||||
parameter" error; more than one match (multiple 1-arg overloads sharing the name) raises an
|
|
||||||
"ambiguous - N overloads take exactly one parameter" error, since `@DtoConvert` has no
|
|
||||||
parameter-type-based way to disambiguate and the developer must rename one of the overloads.
|
|
||||||
- Implemented in `querybean-generator` (`DtoMappingReader.resolveConverter()`,
|
|
||||||
`findConverterMethod()`). Test coverage: new negative compile-error tests
|
|
||||||
`DtoMapperComputedPathTest#dtoConvertMethod_withAmbiguousOverloads_expectCompileError` (two
|
|
||||||
same-named 1-arg overloads) and `#dtoConvertMethod_withWrongArity_expectCompileError` (a
|
|
||||||
same-named 0-arg method, no 1-arg candidate at all); existing `tests/test-dto-mapping`
|
|
||||||
converter usage (a single, unambiguous 1-arg method per converter type) continues to resolve
|
|
||||||
and pass unchanged.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- Requirements: [dto-mapping-requirements.md](./dto-mapping-requirements.md)
|
|
||||||
- Issue: https://github.com/ebean-orm/ebean/issues/2540
|
|
||||||
- MapStruct cycle mapping: https://mapstruct.org/documentation/stable/reference/html/#mapping-object-cycles
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
# Nested DTO Mapping — Requirements
|
|
||||||
|
|
||||||
Design requirements distilled from [issue #2540 "Support nested DTO mapping"](https://github.com/ebean-orm/ebean/issues/2540),
|
|
||||||
reviewed against comparable features in QueryDSL (`@QueryProjection`) and Blaze-Persistence (`@EntityView`).
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
Ebean already supports:
|
|
||||||
|
|
||||||
- Partial/flat DTO queries via `DB.findDto(...)` and `query.select(...).asDto(Dto.class)`.
|
|
||||||
- `@Formula` / `@Formula2` — path-based, auto-joined computed SQL expressions, but only on managed entities.
|
|
||||||
- `query.setUnmodifiable(true)` — builds a read-only, non-lazy-loading entity graph (`InterceptReadOnly`,
|
|
||||||
see PR #2626). Accessing an unloaded property throws `LazyInitializationException`; mutating throws
|
|
||||||
`UnmodifiableEntityException`.
|
|
||||||
|
|
||||||
Unlike Hibernate, Ebean does dirty-detection on the bean itself (no dynamic proxies), so there is very little
|
|
||||||
extra cost to an entity-graph query versus a DTO query. This makes an **unmodifiable entity graph** a cheap,
|
|
||||||
natural intermediate representation to map *from* when producing a DTO graph — we don't need Blaze/Hibernate's
|
|
||||||
proxy-based `EntityView` mechanism to get the performance benefit they are chasing.
|
|
||||||
|
|
||||||
The goal is nested DTO graph support (DTOs containing ToOne/ToMany child DTOs), not just today's flat DTOs,
|
|
||||||
while keeping DTOs as plain, framework-unattached classes.
|
|
||||||
|
|
||||||
## Accepted Requirements
|
|
||||||
|
|
||||||
### A. Nested DTO graphs
|
|
||||||
|
|
||||||
- **Support nested DTO graphs (ToOne/ToMany)**
|
|
||||||
Allow mapping a query result into a DTO graph where DTO fields are themselves DTOs (ToOne) or
|
|
||||||
`List`/`Set<Dto>` (ToMany), not just flat DTOs. Use the existing `setUnmodifiable(true)` entity graph as
|
|
||||||
the intermediate, de-duplicated, identity-consistent source to map from.
|
|
||||||
*Inspiration: Blaze `@EntityView` subviews/subview collections; Jimmer fetcher DTOs.*
|
|
||||||
|
|
||||||
- **Auto-generated entity → DTO graph mapper**
|
|
||||||
Given an unmodifiable entity graph plus a target nested DTO type, generate (via annotation processing,
|
|
||||||
reflection-free) a mapper that walks the graph and populates the DTO graph, matching properties by
|
|
||||||
name/type with override annotations for renames, computed values, and collection element types.
|
|
||||||
*Inspiration: Blaze `@EntityView` + subview mapping; conceptually similar to MapStruct but Ebean-generated
|
|
||||||
and graph/identity aware.*
|
|
||||||
|
|
||||||
- **Identity-aware de-duplication in nested collections**
|
|
||||||
When mapping nested collections referencing the same underlying entity instance multiple times, reuse the
|
|
||||||
same DTO instance (mirrors Blaze/Jimmer identity semantics) rather than producing independent copies.
|
|
||||||
*Inspiration: Blaze/Jimmer identity handling.*
|
|
||||||
|
|
||||||
### B. Formula-style DTO annotations
|
|
||||||
|
|
||||||
- **`@Formula2`-like annotations on DTO fields**
|
|
||||||
Bring the existing `@Formula` / `@Formula2` concept (auto-joined, path-based computed SQL expressions) to
|
|
||||||
DTO classes so a DTO field can request a computed/aggregated value with the join auto-derived, instead of
|
|
||||||
only being available on managed entities.
|
|
||||||
*Inspiration: User suggestion; Ebean `@Formula2`; Blaze `@Mapping` computed expressions.*
|
|
||||||
*Status: a narrower version (pulling in an existing entity-level `@Formula2` by path) was implemented and
|
|
||||||
then rejected - for the common same-name case it generated code identical to a plain unannotated field, so
|
|
||||||
the annotation added no real value beyond a codegen-time validation. See `docs/dto-mapping-design.md`
|
|
||||||
("Formula2-on-DTO scope" and "Ad-hoc computed/formula properties" sections). The broader goal - arbitrary
|
|
||||||
ad-hoc computed SQL on a DTO field - is better served by modelling the computed value as its own
|
|
||||||
`@Entity @View`/`@Sql` read entity and mapping *that* into a plain DTO, reusing the existing (already
|
|
||||||
accepted) nested-DTO mapping machinery rather than a new DTO-level annotation.
|
|
||||||
|
|
||||||
- **Path-based property mapping annotation on DTO**
|
|
||||||
Allow a DTO field or constructor param to be annotated with a source path expression (e.g. `parent.name`)
|
|
||||||
so Ebean can auto-derive the select clause plus joins for nested/renamed properties, reducing manual
|
|
||||||
constructor wiring for non-trivial mappings.
|
|
||||||
*Inspiration: Blaze `@Mapping`; QueryDSL constructor expressions.*
|
|
||||||
|
|
||||||
### C. Compile-time safety
|
|
||||||
|
|
||||||
- **Compile-time verification of `select(...).asDto(...)` mapping** *(explored, rejected as impractical -
|
|
||||||
`mapTo()` accepted as the alternative)*
|
|
||||||
Today `select(props).asDto(Dto.class)` is only checked at runtime. Explored an annotation-processor
|
|
||||||
based mechanism to verify at compile time that selected properties match the DTO constructor or setters,
|
|
||||||
mirroring QueryDSL's `@QueryProjection` compile-time Q-type generation. Rejected as impractical: raw SQL
|
|
||||||
is an opaque `String` at compile time, and even the typed query-bean `.select(...)` form only
|
|
||||||
type-checks against the *entity* - the match to the target DTO still happens at runtime via reflection
|
|
||||||
(`DtoQueryPlanConstructor`), and the `.asDto(...)` call site can be arbitrarily distant from the
|
|
||||||
`.select(...)` call, so there's no fixed AST shape an annotation processor could reliably verify.
|
|
||||||
QueryDSL's actual compile-time safety comes from a different mechanism entirely - typed
|
|
||||||
`Projections.constructor(...)`/generated Q-type constructor calls, not from checking an
|
|
||||||
independently-built select-list against a DTO. `mapTo(Dto.class)` (see section A) already closes the
|
|
||||||
underlying gap in the opposite, tractable direction: it derives the select/fetch spec *from* the DTO's
|
|
||||||
declared shape at APT time, so it is compile-time safe by construction, with no separate select-list to
|
|
||||||
drift out of sync. Recommendation: document `mapTo()` as the compile-time-safe answer for DTO
|
|
||||||
projections, and treat `asDto()`/`findDto()` explicitly as the flexible, runtime-checked escape hatch
|
|
||||||
for raw/dynamic SQL.
|
|
||||||
*Inspiration: QueryDSL `@QueryProjection` compile-time Q-type generation.*
|
|
||||||
|
|
||||||
- **Fail-fast on unmapped or lazy property access**
|
|
||||||
Ensure a clear, documented, minimal-ceremony way to fail fast if code touches a property not included in
|
|
||||||
the query projection, instead of silently lazy loading or returning null. `query.setUnmodifiable(true)`
|
|
||||||
already satisfies this (throws `LazyInitializationException`) — document/promote it as the answer, and
|
|
||||||
evaluate whether a lighter-weight flag decoupled from full unmodifiable/read-only semantics is needed.
|
|
||||||
*Inspiration: Original issue ask; already solved via `setUnmodifiable()` (PR #2626 `InterceptReadOnly`).*
|
|
||||||
|
|
||||||
### D. Fetch strategy and performance
|
|
||||||
|
|
||||||
- **Fetch strategy control for DTO graph relationships**
|
|
||||||
Existing entity query fetch hints (join vs. select/subselect secondary query, `+query`/`+lazy`) should
|
|
||||||
transparently carry over when the target of the query is a DTO graph rather than an entity graph.
|
|
||||||
`query.mapTo(Dto.class)` applies the DTO-derived `FetchGroup` only when the query has no
|
|
||||||
`select()`/`fetch()` already set - a manually tuned fetch spec always takes precedence and is never
|
|
||||||
overridden, allowing manual query optimisation when needed (at the cost of falling back to the
|
|
||||||
existing fail-fast-on-unmapped-property behaviour if the manual spec doesn't cover what the DTO needs).
|
|
||||||
*Inspiration: Blaze FETCH/SELECT/SUBSELECT fetch strategies.*
|
|
||||||
|
|
||||||
- **Pagination support for DTO graph queries**
|
|
||||||
Confirm existing pagination works unchanged when projecting into nested DTO graphs.
|
|
||||||
*Inspiration: Blaze pagination and keyset pagination support.*
|
|
||||||
|
|
||||||
### E. Custom property conversion
|
|
||||||
|
|
||||||
- **Per-property custom scalar conversion (`@DtoConvert`)**
|
|
||||||
Motivated by real hand-written mapper code (`DriverMapper`, central-access) doing per-property scalar
|
|
||||||
coercion (`short` -> `boolean`) and dependency-backed conversion (AES decryption via an injected cipher).
|
|
||||||
Introduce a `@DtoConvert(value = ConverterType.class, method = "name")` annotation (combinable with
|
|
||||||
`@DtoPath` for source-getter override) on a DTO property. The generator dispatches based on whether the
|
|
||||||
referenced method is `static`:
|
|
||||||
- **Static method** -> a direct static call is inlined (`ConverterType.method(source.getX())`), zero
|
|
||||||
ceremony, no registration - covers common, reusable, dependency-free scalar coercions (e.g.
|
|
||||||
`short`/`boolean`, enum <-> `String`) that could apply across many unrelated entity/DTO pairs.
|
|
||||||
- **Instance method** -> dispatched via `DtoConverterManager.get(ConverterType.class).method(source.getX())`
|
|
||||||
and wired as a real constructor parameter/field on the generated mapper (same shape as existing
|
|
||||||
nested-mapper constructor injection) - covers conversions needing a real dependency (e.g. a cipher).
|
|
||||||
`DtoConverterManager` is a small, deliberately-scoped static put/get bridge: the app registers an
|
|
||||||
already-DI-constructed converter singleton (e.g. built by avaje-inject) *before* building the `Database`.
|
|
||||||
This is a narrow, accepted exception to the general no-static-mutable-state convention - it exists solely
|
|
||||||
to bridge an already-DI-constructed singleton into `ServiceLoader`-discovered, no-arg-constructed generated
|
|
||||||
code, which cannot otherwise reach a DI container. `DtoConverterManager.get(type)` throws immediately if
|
|
||||||
nothing was registered, so a missing converter fails fast at Database-startup time (an eager field
|
|
||||||
initializer on the generated `EbeanDtoMapperRegister`), not lazily on first use.
|
|
||||||
*Design exploration considered and rejected two alternatives first: (a) a `DtoMapContext.service(Class)`
|
|
||||||
lookup - rejected because `DtoMapContext` is a short-lived per-call identity-cache only, wrong lifetime for
|
|
||||||
a real singleton dependency; (b) a `ServiceLoader`-discovered `DtoConverterSource` SPI mirroring
|
|
||||||
`DtoMapperRegister` itself - rejected because a `ServiceLoader`-instantiated (no-arg) source cannot bridge
|
|
||||||
to an *already* DI-constructed dependency (e.g. a cipher needing config/secrets) without reconstructing it
|
|
||||||
itself, duplicating/bypassing the app's own DI-managed instance.*
|
|
||||||
*Inspiration: `DriverMapper` (central-access) hand-written pattern; MapStruct qualified converter methods.*
|
|
||||||
*Status: implemented - `@DtoConvert` (ebean-annotation), `io.ebean.DtoConverterManager` (ebean-api), and
|
|
||||||
querybean-generator codegen support (static/instance dispatch, constructor wiring deduplicated by converter
|
|
||||||
type). Test coverage: `tests/test-dto-mapping` `TestDtoConvert`.*
|
|
||||||
|
|
||||||
- **Type-pair (package-level) custom scalar conversion**
|
|
||||||
Motivated by real hand-written mapper code (`EboxMapper`, central-access): the same conversion repeats
|
|
||||||
across many unrelated properties on one target - `DateUtils.toCalendar(...)` on ~9 fields,
|
|
||||||
`parseEnum(EnumType.class, value)` on ~3 - under today's `@DtoConvert` every one of those properties must
|
|
||||||
carry its own repeated annotation. MapStruct solves this by letting a conversion method be defined once
|
|
||||||
(in the mapper or a `uses = {...}` helper) and auto-applying it to *every* property whose source/target
|
|
||||||
types match that method's signature - no per-field wiring. Proposed: a package-level, repeatable
|
|
||||||
`@DtoConverters({ConverterType.class, ...})` (sibling to `@DtoMapping` in `package-info.java`) - the
|
|
||||||
generator indexes every public static/instance method on the referenced type(s) by `(paramType ->
|
|
||||||
returnType)`, then for any property whose source getter type doesn't already match the target field type
|
|
||||||
and which carries no explicit per-property `@DtoConvert`, looks up that type pair and wires it in
|
|
||||||
automatically (same static-vs-instance/`DtoConverterManager` dispatch rules as `@DtoConvert` today). An
|
|
||||||
explicit per-property `@DtoConvert` always overrides the type-level default. Deliberately no built-in
|
|
||||||
conversions shipped by Ebean itself (no implicit `Enum.valueOf`/`.name()`) - the app still owns
|
|
||||||
exception/null-handling semantics (e.g. `parseEnum`'s catch-and-null-on-bad-value), just declares it once
|
|
||||||
instead of per-field.
|
|
||||||
**Status: implemented.** `@DtoConverters(ConverterType.class, ...)` (a single non-repeatable annotation
|
|
||||||
taking a `Class<?>[]`, `@Target({PACKAGE, MODULE})`) is registered once per package/module alongside
|
|
||||||
`@DtoMapping`. The generator indexes every public, single-arg, non-void method on each referenced type by
|
|
||||||
exact `(paramType -> returnType)`; any SCALAR property (plain or `@DtoPath`-renamed) with no explicit
|
|
||||||
`@DtoConvert` and a source/target type mismatch is auto-wired to the matching method (a duplicate/ambiguous
|
|
||||||
type pair across the registered types is a compile-time processor error). List-element-wise conversion and
|
|
||||||
`@DtoRef` (FK-id) properties are out of scope. Test coverage:
|
|
||||||
`tests/test-dto-mapping/.../TestDtoConverters.java` (`UuidConverters`/`UuidShortCodeConverter`,
|
|
||||||
`ContactTypeConverterDto`) - covers same-name auto-dispatch, `@DtoPath`-renamed auto-dispatch, and explicit
|
|
||||||
`@DtoConvert` overriding the registered default.
|
|
||||||
*Inspiration: `EboxMapper` (central-access) hand-written pattern; MapStruct type-signature-matched
|
|
||||||
conversion methods.*
|
|
||||||
|
|
||||||
- **`@DtoMixin` for DTOs that cannot be annotated directly**
|
|
||||||
Some DTOs are generated (e.g. from an OpenAPI spec) and not editable/annotatable, so `@DtoPath`/
|
|
||||||
`@DtoConvert`/`@DtoRef` cannot always be placed directly on the DTO. Introduce a `@DtoMixin(Target.class)`
|
|
||||||
companion interface/type, discovered by scanning the compilation round and overlaying its per-property
|
|
||||||
annotations onto the real target's properties by name-match - directly mirrors avaje-jsonb's
|
|
||||||
`@Json.MixIn` mechanism (`KingfisherMixin`/`CrewMateMixIn` pattern), a proven prior-art solution to the
|
|
||||||
exact same "can't annotate a generated/unowned type" problem.
|
|
||||||
*Inspiration: avaje-jsonb `@Json.MixIn`.*
|
|
||||||
*Status: implemented - `@DtoMixin` (ebean-annotation) and querybean-generator round-scanning/overlay
|
|
||||||
support (matches mixin methods to target properties by name, applying whichever of `@DtoPath`/`@DtoRef`/
|
|
||||||
`@DtoConvert` is present as if declared on the target field itself). Test coverage: `tests/test-dto-mapping`
|
|
||||||
`TestDtoMixin`.*
|
|
||||||
|
|
||||||
### F. DI-friendly manual mapper usage
|
|
||||||
|
|
||||||
- **Public `DtoMapperManager` with `get(Class<T> mapperType)` for DI**
|
|
||||||
Motivated by `DriverMapper`/`DriverService` (central-access): `DriverMapper` is a hand-written
|
|
||||||
`@Component` constructor-injected into `DriverService`. Moved `DtoMapperManager` from internal
|
|
||||||
(`io.ebeaninternal.server.dto`) to public `io.ebean` - unchanged `mapperFor(source, dto)`, plus a new
|
|
||||||
`get(Class<T> mapperType)` keyed by the generated mapper's own concrete class (e.g.
|
|
||||||
`manager.get(CustomerDtoMapper.class)`), for direct/concrete-typed DI injection. `DtoMapperRegister`
|
|
||||||
gained a default `mapperOfType(Class<T>)` method (non-breaking); the generator emits the real if-chain
|
|
||||||
body (mirrors `mapperFor`'s if-chain). `DtoMapperManager` has zero `Database` dependency (constructor
|
|
||||||
only does `ServiceLoader.load(DtoMapperRegister.class)`), so it can be constructed standalone,
|
|
||||||
independent of/before a `Database` - e.g. as an avaje-inject bean.
|
|
||||||
*Inspiration: `DriverMapper`/`DriverService` (central-access).*
|
|
||||||
*Status: implemented.*
|
|
||||||
|
|
||||||
- **`DtoMapperManager` sharing via `DatabaseBuilder.putServiceObject`**
|
|
||||||
So `query.mapTo()` and application-injected mappers share the exact same `DtoMapperManager` instance
|
|
||||||
(and hence the same underlying generated mapper singletons) rather than each independently constructing
|
|
||||||
its own, `InternalConfiguration` checks `config.getServiceObject(DtoMapperManager.class)` first (mirrors
|
|
||||||
the existing `AutoMigrationRunner`/`GeoTypeProvider` `putServiceObject`/`getServiceObject` pattern),
|
|
||||||
falling back to constructing a default `new DtoMapperManager()` if none was supplied.
|
|
||||||
*Inspiration: user proposal following `DriverMapper`/`DriverService` review.*
|
|
||||||
*Status: implemented.*
|
|
||||||
|
|
||||||
*Rejected: generator-emitted `builder(source)` method* - `DriverMapper` exposes `builder(cDriver)`
|
|
||||||
returning a partially-populated `DriverBuilder` so callers can add extra caller-supplied fields (e.g.
|
|
||||||
fleets) before `build()`. Rejected as a generator feature - `Driver`/`DriverSummary` already use
|
|
||||||
avaje-recordbuilder's `@RecordBuilder`, which generates `Target.builder(existingInstance)`
|
|
||||||
(seed-from-instance). The same effect is already achievable with zero ebean changes:
|
|
||||||
`mapper.map(source)` then `Builder.builder(mapped).extraField(x).build()`. Documented as a recipe
|
|
||||||
instead (see "Recipe: adding extra caller-supplied fields after mapping" in
|
|
||||||
`docs/guides/mapping-entity-graphs-to-dtos.md`).
|
|
||||||
|
|
||||||
### G. Large-target construction and shape variants
|
|
||||||
|
|
||||||
- **Builder-based target construction for large DTOs**
|
|
||||||
Motivated by `UserService`/`User` (central-access): `User` is a 24-field OpenAPI-generated record with
|
|
||||||
a `@RecordBuilder`-generated `UserBuilder`, hand-mapped via a long fluent builder chain rather than a
|
|
||||||
positional constructor to stay readable/refactor-safe. The generator auto-detects a RecordBuilder-style
|
|
||||||
builder on the target (static `Target.builder()` + fluent per-property setters + `build()`) and uses
|
|
||||||
`Target.builder().prop(x)....build()` instead of `new Target(a, b, c, ...)` whenever (a) a builder is
|
|
||||||
detected and (b) the target has more than a threshold number of properties (default 5), falling back to
|
|
||||||
the positional constructor otherwise. An explicit `@DtoMapping` attribute (`builder = AUTO | ALWAYS |
|
|
||||||
NEVER`) overrides the heuristic in either direction. Applies regardless of whether the target class is
|
|
||||||
hand-authored or foreign/generated (e.g. an OpenAPI record) - `@DtoMapping` is already declared
|
|
||||||
externally via `package-info.java`, not on the target class, so this was already compatible with
|
|
||||||
foreign target types.
|
|
||||||
*Inspiration: `UserService`/`User` (central-access).*
|
|
||||||
*Status: implemented.*
|
|
||||||
|
|
||||||
- **Named mapper variants excluding nested paths, sharing one generated class**
|
|
||||||
Motivated by `UserService`/`User` (central-access): `CUser` -> `User` is mapped in two shapes - with
|
|
||||||
nested `fleets` (`findUserByGid`) and without (`findAll`, bulk listing) - to avoid an unnecessary
|
|
||||||
join/fetch on the common bulk-listing path. Keeps the existing "shape always derived from declaration,
|
|
||||||
fetch spec always wins" philosophy (rejected relaxing that rule / rejected a runtime
|
|
||||||
is-property-loaded auto-skip check as less deterministic). The same `(source, target)` pair can be
|
|
||||||
declared more than once in `package-info.java` via a named variant, e.g.
|
|
||||||
`@DtoMapping(source = CUser.class, target = User.class)` (base/full) plus
|
|
||||||
`@DtoMapping(source = CUser.class, target = User.class, name = "noFleets", exclude = "fleets")`
|
|
||||||
(variant). Both variants are generated into the **same** mapper class (one class per target, not one
|
|
||||||
per variant) and share a single private `build(source, context, boolean includeXxx, ...)` method
|
|
||||||
containing the common field population written once; each excluded nested path becomes a `boolean
|
|
||||||
includeXxx` parameter of that shared method rather than a precomputed value, so `build()` still
|
|
||||||
evaluates every property - included or excluded - inline, at its own declared field position (a
|
|
||||||
`includeFleets ? fleetsMapper.mapList(...) : List.of()` ternary in place, not hoisted out as a
|
|
||||||
pre-evaluated call argument). This preserves the DTO's declared property order as the true evaluation
|
|
||||||
order regardless of which properties a variant happens to exclude. The base `map()` passes `true` for
|
|
||||||
every flag; each named variant (exposed as a same-named accessor, e.g. `noFleets()`, returning a single
|
|
||||||
shared/cached instance of its own small `DtoMapper<SOURCE, TARGET>`-implementing inner class - not
|
|
||||||
reconstructed per call) passes `false` for the paths it excludes and omits that path from its own
|
|
||||||
`fetchGroup`. Selected via a new `query.mapTo(Class<D> dtoType, DtoMapper<T, D> mapper)` overload
|
|
||||||
taking an already-resolved mapper instance directly (e.g. `query.mapTo(User.class,
|
|
||||||
userMapper.noFleets())`) - no string-based variant lookup, and no changes needed to
|
|
||||||
`DtoMapperRegister`/`DtoMapperManager`.
|
|
||||||
*Inspiration: `UserService`/`User` (central-access).*
|
|
||||||
*Status: implemented.*
|
|
||||||
|
|
||||||
- **Setter-based (mutable JavaBean) target construction**
|
|
||||||
Motivated by `EboxMapper` (central-access): its target types (`Ebox`, `MachineSummaryInfo`, from
|
|
||||||
`nz.co.eroad.schema.eroadtypes`, JAXB/XSD-generated legacy SOAP shapes) are plain mutable JavaBeans - a
|
|
||||||
public no-arg constructor plus a `void setXxx(...)` setter per property - neither a positional constructor
|
|
||||||
match nor a RecordBuilder-style fluent builder (see section G above). The generator currently only
|
|
||||||
recognizes those two construction strategies, so this common third shape (typical of JAXB/XSD-generated
|
|
||||||
and many hand-written mutable POJOs) can't be targeted by `@DtoMapping` at all today. Proposed: detect a
|
|
||||||
no-arg constructor plus a `void setXxx(propertyType)` setter per mapped property as a third construction
|
|
||||||
strategy, generating `Target target = new Target(); target.setX(...); ...; return target;` (mirroring the
|
|
||||||
existing `build = AUTO | ALWAYS | NEVER` override precedent from section G for explicit control over which
|
|
||||||
strategy applies). Would also unblock the `mapToBuilder()`-style "populate ignored/derived properties after
|
|
||||||
the generated mapping, before finishing construction" pattern for these targets (currently only available
|
|
||||||
for builder-shaped targets) - relevant to `EboxMapper`'s `machineSummaryInfo` (a genuinely composite,
|
|
||||||
multi-association derived value, out of reach of `@DtoConvert`/`@DtoPath` regardless of this gap, but a
|
|
||||||
natural fit for the same "map base fields via codegen, then set the derived one by hand" pattern already
|
|
||||||
used for `Fleet.assignedMachines`/`assignedDrivers`).
|
|
||||||
*Inspiration: `EboxMapper` (central-access); JAXB/XSD-generated SOAP DTO shapes generally.*
|
|
||||||
**Status: implemented.** `@DtoMapping(setter = AUTO | ALWAYS | NEVER)` mirrors `builder()`'s override
|
|
||||||
precedent. Detection requires a public no-arg constructor plus a public `setXxx(...)` setter for every
|
|
||||||
mapped property - either `void` or fluent-style (returning the target type itself, e.g. `public Target
|
|
||||||
setXxx(...) { ...; return this; }`); the generated code always calls the setter as a bare statement and
|
|
||||||
discards any return value, so either shape works identically. A builder, when selected, always takes
|
|
||||||
priority over setter-based construction. Under the default `AUTO`, setter-based construction is only
|
|
||||||
attempted when the target has no positional constructor matching the mapped properties (arity-based) and
|
|
||||||
no builder was selected - existing positional-constructor and builder-shaped targets are entirely
|
|
||||||
unaffected. `ALWAYS` requires the shape (codegen-time error otherwise); `NEVER` always uses a positional
|
|
||||||
constructor. Generated shape: `Target target = new Target(); target.setX(...); ...; return target;` (a
|
|
||||||
`computeIfAbsent(...)`-wrapped block-lambda variant when the target is nested elsewhere in the graph).
|
|
||||||
Deliberately **no** `mapToBuilder(...)`-style post-construction accessor is generated for this strategy -
|
|
||||||
the returned target is already the final, fully mutable instance (setters are required to be `public`), so
|
|
||||||
a caller can already call e.g. `dto.setExternalRef(...)` directly on the mapped result, exactly the pattern
|
|
||||||
`EboxMapper` already uses by hand; this is unlike the builder strategy, where the intermediate builder is
|
|
||||||
otherwise unreachable after its one-shot `build()` call. Test coverage:
|
|
||||||
`tests/test-dto-mapping/.../TestDtoSetterConstruction.java` (`ContactSetterDto`) - covers auto-detected
|
|
||||||
setter-chain construction plus post-construction population of two `@DtoIgnore` properties (a plain scalar
|
|
||||||
and a `List`) via their public setters; plus `ContactSetterFluentDto` - covers the fluent-setter-return-shape
|
|
||||||
variant.
|
|
||||||
|
|
||||||
### H. Record entity sources
|
|
||||||
|
|
||||||
- **Record-style (bare/fluent) accessors on the source (entity) side**
|
|
||||||
Ebean supports entity beans declared as Java `record`s (e.g. `public record CourseRecordEntity(@Id long id,
|
|
||||||
String name, String notes) {}` - see `test-java16`), whose only accessor shape is the bare component name
|
|
||||||
(`active()`, `name()`, `id()`) - never `getXxx()`/`isXxx()`. This bare-accessor convention isn't limited to
|
|
||||||
an actual `record` type though - an ordinary class can just as easily expose bare/fluent-style accessors
|
|
||||||
with no `get`/`is` prefix at all. The generator resolves the real accessor for each source type (the direct
|
|
||||||
source, or an intermediate `@DtoPath`/`@DtoRef` association type) by checking which shape actually exists as
|
|
||||||
a method, in order: (1) `isXxx()` returning `boolean` (JavaBean boolean convention), (2) `getXxx()` (JavaBean
|
|
||||||
convention), (3) the bare `propertyName()` itself - falling back to a guessed `getXxx()` only if none of the
|
|
||||||
three are found. Resolution is entirely name/existence-based (no dependency on whether the type is actually
|
|
||||||
a `record`). The Ebean bean-property name used in generated `FetchGroup.select(...)`/`.fetch(...)` calls is
|
|
||||||
tracked directly from the original property/segment name (not reverse-parsed from the resolved accessor's
|
|
||||||
method name), so it's correct regardless of which of the three accessor shapes was used.
|
|
||||||
*Inspiration: Ebean's own record-entity support (`test-java16`); user-reported gap during review.*
|
|
||||||
*Status: implemented.*
|
|
||||||
|
|
||||||
## Rejected Requirements
|
|
||||||
|
|
||||||
These were considered and explicitly rejected as out of scope:
|
|
||||||
|
|
||||||
- **DTO as interface / dynamic proxy views** — Blaze `@EntityView` defines views as interfaces backed by
|
|
||||||
runtime proxies. This conflicts with Ebean's preference for plain, framework-unattached DTO classes.
|
|
||||||
- **Updatable or creatable entity views (persist through DTO)** — Blaze's `@UpdatableEntityView` /
|
|
||||||
`@CreatableEntityView` cascade persist/update through the view. This would duplicate Ebean's existing
|
|
||||||
entity persistence model and introduce a second, ambiguous dirty-checking/cascade model.
|
|
||||||
- **New predicate/filter DSL for subview collections** — Blaze allows filter expressions directly in
|
|
||||||
`@Mapping` (e.g. filtering a collection by an attribute value). Ebean already has typed query bean
|
|
||||||
predicates and `.filterMany()` for filtering child collections in queries; no new embedded filter
|
|
||||||
expression language is needed on the DTO itself.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- Issue: https://github.com/ebean-orm/ebean/issues/2540
|
|
||||||
- PR #2626: `InterceptReadOnly` / `InterceptReadWrite` split enabling the unmodifiable entity graph fast path
|
|
||||||
- Ebean docs: https://ebean.io/docs/query/option#unmodifiable
|
|
||||||
- QueryDSL: `@QueryProjection` (constructor-based, compile-time-checked projections)
|
|
||||||
- Blaze-Persistence Entity Views: https://persistence.blazebit.com/documentation/1.6/entity-view/manual/en_US/
|
|
||||||
@@ -1,26 +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
|
|
||||||
- Mapping entity graphs to DTOs (`mapTo`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/mapping-entity-graphs-to-dtos.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,208 +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 |
|
|
||||||
| [Mapping entity graphs to DTOs (`mapTo`)](mapping-entity-graphs-to-dtos.md) | Map a nested entity graph query result to a nested DTO graph via `query.mapTo(Dto.class)`; `@DtoPath`/`@DtoRef` for renamed/flattened/id-only properties; identity-aware de-dup via `DtoMapContext`; computed/aggregate DTO values via `@Entity @View` + `@Formula2`/`@Sum`/`@Aggregation`; comparison with the flat `asDto()` pipeline |
|
|
||||||
| [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
|
|
||||||
- Mapping entity graphs to DTOs (`mapTo`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/mapping-entity-graphs-to-dtos.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
|
|
||||||
- Mapping entity graphs to DTOs (`mapTo`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/mapping-entity-graphs-to-dtos.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.11</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,785 +0,0 @@
|
|||||||
# Guide: Mapping entity graphs to DTOs — `query.mapTo(Dto.class)`
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
`query.mapTo(SomeDto.class)` maps an entity query result to a **nested DTO graph** —
|
|
||||||
DTO fields can themselves be DTOs (`ToOne`) or `List<Dto>`/`Set<Dto>` (`ToMany`), not
|
|
||||||
just flat scalar columns. Ebean generates the mapper (reflection-free), automatically
|
|
||||||
derives the query's `select()`/`fetch()` spec from the target DTO's declared shape, and
|
|
||||||
forces `setUnmodifiable(true)` so any property the mapper needs but wasn't fetched fails
|
|
||||||
fast with `LazyInitialisationException` instead of silently lazy loading.
|
|
||||||
|
|
||||||
This is distinct from the existing flat `asDto(Dto.class)` — see
|
|
||||||
[Quick comparison](#quick-comparison-mapto-vs-asdto-vs-plain-entity-query) below.
|
|
||||||
|
|
||||||
```java
|
|
||||||
Optional<CustomerDto> dto = new QCustomer()
|
|
||||||
.id.eq(customerId)
|
|
||||||
.mapTo(CustomerDto.class)
|
|
||||||
.findOneOrEmpty();
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
List<CustomerDto> dtos = DB.find(Customer.class)
|
|
||||||
.where().eq("status", Status.ACTIVE)
|
|
||||||
.mapTo(CustomerDto.class) // no .select()/.fetch() needed - derived from CustomerDto's shape
|
|
||||||
.findList();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick comparison: `mapTo()` vs `asDto()` vs plain entity query
|
|
||||||
|
|
||||||
| | `mapTo(Dto.class)` | `asDto(Dto.class)` | Plain entity query |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Shape | Nested DTO **graph** (ToOne/ToMany) | Flat, single-row DTO | Entity graph |
|
|
||||||
| Fetch spec | Auto-derived from the DTO's declared shape | Whatever `select()`/SQL you write | Whatever `select()`/`fetch()` you write |
|
|
||||||
| Mismatch caught | At compile time (unregistered pair fails fast at first use; codegen fails fast on structural problems) | At runtime (reflection-based constructor/setter matching) | N/A (real entity properties) |
|
|
||||||
| Identity/de-dup | Yes - repeated source instances map to the same DTO instance (`DtoMapContext`) | N/A (one row in, one DTO out) | Yes (entity/persistence-context identity) |
|
|
||||||
| Backing pipeline | Executes the entity ORM query, `setUnmodifiable(true)`, maps the resulting graph | Executes SQL directly against a flat `ResultSet` | Executes the entity ORM query |
|
|
||||||
| Best for | API/read-model responses that mirror a **nested** entity shape | Flat summary rows, reports, native/vendor SQL | Data you intend to mutate and save back |
|
|
||||||
|
|
||||||
See also [writing-ebean-query-beans.md](writing-ebean-query-beans.md) (Step 8/9) for
|
|
||||||
`asDto()` and the general query-shape decision guide.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Basic usage
|
|
||||||
|
|
||||||
### 1. Declare a plain DTO
|
|
||||||
|
|
||||||
DTOs are plain classes with **no framework attachment** — no annotations required for
|
|
||||||
the common case (properties matched to the source entity by name):
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class CustomerDto {
|
|
||||||
private final Long id;
|
|
||||||
private final String name;
|
|
||||||
private final AddressDto billingAddress; // nested ToOne
|
|
||||||
private final List<ContactDto> contacts; // nested ToMany
|
|
||||||
|
|
||||||
public CustomerDto(Long id, String name, AddressDto billingAddress, List<ContactDto> contacts) {
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
this.billingAddress = billingAddress;
|
|
||||||
this.contacts = contacts;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() { return id; }
|
|
||||||
public String getName() { return name; }
|
|
||||||
public AddressDto getBillingAddress() { return billingAddress; }
|
|
||||||
public List<ContactDto> getContacts() { return contacts; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
A constructor whose parameters match (by name) a source entity/DTO property is used
|
|
||||||
for mapping — same shape convention as the existing `DtoQuery`. Getters are used to
|
|
||||||
read the source's properties — a bare/fluent accessor like `active()` is resolved
|
|
||||||
automatically too, not just `getActive()`/`isActive()` (useful both for Ebean's own
|
|
||||||
record entity beans and for ordinary classes that just expose bare-name accessors).
|
|
||||||
|
|
||||||
### 2. Register the (source, target) pair
|
|
||||||
|
|
||||||
Declare each entity → DTO pair with `@DtoMapping` on a `package-info.java` (a neutral
|
|
||||||
holder — see [Why `package-info.java`?](#why-package-infojava)):
|
|
||||||
|
|
||||||
```java
|
|
||||||
@DtoMapping(source = Customer.class, target = CustomerDto.class)
|
|
||||||
@DtoMapping(source = Address.class, target = AddressDto.class)
|
|
||||||
@DtoMapping(source = Contact.class, target = ContactDto.class)
|
|
||||||
package org.example.dto;
|
|
||||||
|
|
||||||
import io.ebean.annotation.DtoMapping;
|
|
||||||
```
|
|
||||||
|
|
||||||
This triggers `querybean-generator` (the existing annotation processor) to generate a
|
|
||||||
`CustomerDtoMapper implements DtoMapper<Customer, CustomerDto>` for each pair — no new
|
|
||||||
Maven/Gradle setup beyond what query beans already require.
|
|
||||||
|
|
||||||
### 3. Query with `mapTo(...)`
|
|
||||||
|
|
||||||
```java
|
|
||||||
List<CustomerDto> dtos = DB.find(Customer.class)
|
|
||||||
.where().eq("status", Status.ACTIVE)
|
|
||||||
.mapTo(CustomerDto.class)
|
|
||||||
.findList();
|
|
||||||
|
|
||||||
CustomerDto one = new QCustomer().id.eq(id).mapTo(CustomerDto.class).findOne();
|
|
||||||
|
|
||||||
Optional<CustomerDto> maybe = new QCustomer().id.eq(id).mapTo(CustomerDto.class).findOneOrEmpty();
|
|
||||||
```
|
|
||||||
|
|
||||||
`mapTo(...)` works the same from a query bean (`QCustomer`) or a plain `DB.find(...)`/
|
|
||||||
`ExpressionList` query.
|
|
||||||
|
|
||||||
### Paging - `findPagedList()`
|
|
||||||
|
|
||||||
`findPagedList()` mirrors `Query#findPagedList()` — the underlying entity query is paged
|
|
||||||
as normal and each page's result is mapped to the target DTO list:
|
|
||||||
|
|
||||||
```java
|
|
||||||
PagedList<CustomerDto> paged = DB.find(Customer.class)
|
|
||||||
.where().eq("status", Status.ACTIVE)
|
|
||||||
.orderBy().asc("name")
|
|
||||||
.setFirstRow(0)
|
|
||||||
.setMaxRows(50)
|
|
||||||
.mapTo(CustomerDto.class)
|
|
||||||
.findPagedList();
|
|
||||||
|
|
||||||
int totalRowCount = paged.getTotalCount(); // page metadata - unaffected by DTO mapping
|
|
||||||
List<CustomerDto> page1 = paged.getList(); // mapped DTOs for this page
|
|
||||||
```
|
|
||||||
|
|
||||||
Page metadata (`getTotalCount()`, `getTotalPageCount()`, `hasNext()`, `hasPrev()`,
|
|
||||||
`loadCount()`, ...) reflects the underlying entity query directly; only `getList()`
|
|
||||||
is mapped (once, cached) to the DTO type.
|
|
||||||
|
|
||||||
### An unregistered pair fails fast
|
|
||||||
|
|
||||||
If `(Customer.class, SomeDto.class)` was never declared via `@DtoMapping`, the first
|
|
||||||
`mapTo(SomeDto.class)` call throws immediately:
|
|
||||||
|
|
||||||
```
|
|
||||||
PersistenceException: No DtoMapper registered mapping Customer -> SomeDto
|
|
||||||
- check @DtoMapping(source = Customer.class, target = SomeDto.class) is declared
|
|
||||||
on a package-info.java processed by querybean-generator
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Auto-derived fetch spec
|
|
||||||
|
|
||||||
You never write `.select()`/`.fetch()` for a `mapTo(...)` query — the generated mapper
|
|
||||||
exposes a `fetchGroup()` built directly from the DTO's declared shape, and `mapTo(...)`
|
|
||||||
applies it automatically:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public CustomerDtoMapper() {
|
|
||||||
this(new AddressDtoMapper(), new ContactDtoMapper());
|
|
||||||
}
|
|
||||||
|
|
||||||
public CustomerDtoMapper(DtoMapper<Address, AddressDto> billingAddressMapper,
|
|
||||||
DtoMapper<Contact, ContactDto> contactsMapper) {
|
|
||||||
this.fetchGroup = FetchGroup.of(Customer.class)
|
|
||||||
.select("id,name")
|
|
||||||
.fetch("billingAddress", billingAddressMapper.fetchGroup())
|
|
||||||
.fetch("contacts", contactsMapper.fetchGroup())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Each nested DTO gets its own generated mapper (mirroring MapStruct's per-type mapper
|
|
||||||
generation), wired together via constructor injection — mappers are stateless and
|
|
||||||
substitutable, not static singletons. Mapper instances are constructed once, in
|
|
||||||
dependency order, and reused — see [DtoMapperManager](#one-mapper-instance-per-pair)
|
|
||||||
below.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Nested collections and identity-aware de-duplication
|
|
||||||
|
|
||||||
When the same source entity instance is reachable via more than one path in the graph
|
|
||||||
(e.g. two `Contact`s sharing the same `Customer`, or the same `Address` referenced from
|
|
||||||
two paths), the mapper reuses the **same** target DTO instance rather than creating
|
|
||||||
duplicate-but-equal copies — mirroring the identity semantics the entity graph already
|
|
||||||
has:
|
|
||||||
|
|
||||||
```java
|
|
||||||
List<CustomerDto> dtos = DB.find(Customer.class).mapTo(CustomerDto.class).findList();
|
|
||||||
|
|
||||||
CustomerDto customer = dtos.get(0);
|
|
||||||
// both contacts share the exact same customer.billingAddress AddressDto instance
|
|
||||||
assertThat(customer.getContacts().get(0).getCustomer())
|
|
||||||
.isSameAs(customer.getContacts().get(1).getCustomer());
|
|
||||||
```
|
|
||||||
|
|
||||||
This is done via a `DtoMapContext` threaded through every nested `map(...)` call within
|
|
||||||
one top-level `mapList(...)`/`findList()` invocation. The generated code only pays for
|
|
||||||
this when it can actually matter — a DTO that's never nested under another DTO skips
|
|
||||||
`DtoMapContext` entirely (there's nothing else in scope to de-duplicate against):
|
|
||||||
|
|
||||||
```java
|
|
||||||
// AddressDto is nested under CustomerDto (reachable via multiple contacts) - dedup needed
|
|
||||||
// dedup using DtoMapContext, same Address instance can be reached via more than one path in the graph
|
|
||||||
return context.computeIfAbsent(AddressDto.class, source, s -> new AddressDto(...));
|
|
||||||
|
|
||||||
// ContactSummaryDto is only ever mapped as a top-level query result - no dedup possible
|
|
||||||
// skip DtoMapContext, only ever a top-level mapping
|
|
||||||
return new ContactSummaryDto(source.getId(), source.getFullName());
|
|
||||||
|
|
||||||
// CustomerDto has nested mappers (billingAddress, contacts) but is never itself nested
|
|
||||||
// DtoMapContext for nested mappers only
|
|
||||||
return new CustomerDto(source.getId(), source.getName(), ...);
|
|
||||||
```
|
|
||||||
|
|
||||||
The generated comment tells you at a glance which of the three cases applies — useful
|
|
||||||
when debugging why a `DtoMapContext` is (or isn't) in the generated code for a
|
|
||||||
particular mapper.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Using generated mappers directly (outside `query.mapTo()`)
|
|
||||||
|
|
||||||
Every generated `XxxDtoMapper` is a plain public class — you don't need `ServiceLoader`,
|
|
||||||
a registry, or a `Database` just to construct or call one directly (though
|
|
||||||
`DtoMapperManager`, below, is available if you want a shared, DI-friendly lookup). It
|
|
||||||
always has a public no-arg constructor (delegating to defaults for any nested mappers/
|
|
||||||
`@DtoConvert` converters) plus an explicit constructor taking those dependencies directly,
|
|
||||||
and implements `DtoMapper<SOURCE, TARGET>`'s `map(...)`/`mapList(...)`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
CustomerDtoMapper mapper = new CustomerDtoMapper();
|
|
||||||
CustomerDto dto = mapper.map(customer); // any Customer you already have on hand
|
|
||||||
List<CustomerDto> dtos = mapper.mapList(customers);
|
|
||||||
```
|
|
||||||
|
|
||||||
This works on **any** entity graph, not just one that just came out of a `mapTo(...)`
|
|
||||||
query — e.g. entities you loaded with a plain `.fetch(...)` query, entities you just
|
|
||||||
`.save()`d, or entities built by hand in a test. The only requirement is that whatever the
|
|
||||||
mapper reads (via plain getters) is actually populated — there's no lazy-loading fallback.
|
|
||||||
|
|
||||||
### Testing the mapping in isolation
|
|
||||||
|
|
||||||
Because mappers are plain, constructor-injected classes, you can unit test the mapping
|
|
||||||
logic itself — independent of `query.mapTo()`, the DTO-pair registry, and (for
|
|
||||||
`@DtoConvert` instance-dispatch converters) `DtoConverterManager` — by passing a test
|
|
||||||
double straight into the explicit constructor:
|
|
||||||
|
|
||||||
```java
|
|
||||||
SecretCipher upperCasingTestCipher = String::toUpperCase;
|
|
||||||
ContactConversionDto dto = new ContactConversionDtoMapper(upperCasingTestCipher).map(contact);
|
|
||||||
|
|
||||||
assertThat(dto.getSecretCode()).isEqualTo("SHH");
|
|
||||||
```
|
|
||||||
|
|
||||||
No `DtoConverterManager.put(...)` registration needed for this kind of test — the real
|
|
||||||
production wiring (`DtoConverterManager.get(SecretCipher.class)`) only happens in the
|
|
||||||
generated no-arg constructor, which the explicit-constructor call above bypasses entirely.
|
|
||||||
See `TestCustomerDtoGraphMapping` (mapper called directly against a manually queried
|
|
||||||
graph) and `TestMapperManualUsage` (mapper called directly against hand-built/just-saved
|
|
||||||
entities, plus the converter test-double case above) in `tests/test-dto-mapping`.
|
|
||||||
|
|
||||||
### `DtoMapperManager` — resolving a generated mapper for dependency injection
|
|
||||||
|
|
||||||
`new CustomerDtoMapper()` is enough for a single mapper, but if your application wants a
|
|
||||||
single shared instance of *every* generated mapper (mirroring how `query.mapTo()` resolves
|
|
||||||
them internally) - e.g. to wire one up for constructor injection into a service, replacing
|
|
||||||
a hand-written mapper class - use `io.ebean.DtoMapperManager`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
DtoMapperManager manager = new DtoMapperManager(); // ServiceLoader discovery only - no Database needed
|
|
||||||
CustomerDtoMapper mapper = manager.get(CustomerDtoMapper.class);
|
|
||||||
```
|
|
||||||
|
|
||||||
`DtoMapperManager` has no dependency on `Database` at all - its constructor only does
|
|
||||||
`ServiceLoader.load(DtoMapperRegister.class)` - so it can be constructed independently,
|
|
||||||
before (or entirely without) a `Database`, e.g. as a bean in an avaje-inject (or any DI
|
|
||||||
framework's) dependency graph:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Factory
|
|
||||||
class DtoMapperFactory {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
DtoMapperManager dtoMapperManager() {
|
|
||||||
return new DtoMapperManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
CustomerDtoMapper customerDtoMapper(DtoMapperManager manager) {
|
|
||||||
return manager.get(CustomerDtoMapper.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If you also want `query.mapTo(...)` to use that *exact same* manager instance (so there's
|
|
||||||
only ever one instance of each generated mapper, whichever path resolves it), register it
|
|
||||||
via `DatabaseBuilder.putServiceObject` before building the `Database` - this is the same
|
|
||||||
`putServiceObject`/`getServiceObject` mechanism already used for things like
|
|
||||||
`AutoMigrationRunner`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
DtoMapperManager sharedManager = new DtoMapperManager();
|
|
||||||
|
|
||||||
Database db = Database.builder()
|
|
||||||
.putServiceObject(DtoMapperManager.class, sharedManager)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// query.mapTo(...) against `db` now resolves mappers via `sharedManager`
|
|
||||||
```
|
|
||||||
|
|
||||||
If nothing is registered via `putServiceObject`, the `Database` builds its own default
|
|
||||||
`DtoMapperManager` instance instead - registering one is entirely optional. A standalone
|
|
||||||
`DtoMapperManager()` construction bypasses the `DatabaseConfigProvider` hook (that hook is
|
|
||||||
specifically about `Database` startup ordering), so if any of your mappers need a
|
|
||||||
`@DtoConvert` instance-dispatch converter, register it via `DtoConverterManager.put(...)`
|
|
||||||
yourself first, exactly as you would before building a `Database`. See
|
|
||||||
`TestDtoMapperManager` and `TestDtoMapperManagerSharing` in `tests/test-dto-mapping`.
|
|
||||||
|
|
||||||
### Recipe: adding extra caller-supplied fields after mapping
|
|
||||||
|
|
||||||
Sometimes a target DTO needs a field that isn't sourced from the entity graph at all - e.g.
|
|
||||||
populated from a separate query or business rule, only when a caller-supplied flag is set.
|
|
||||||
Rather than the generator supporting partial/builder-based mapping directly, if your DTO is
|
|
||||||
a record with a "seed from instance" builder (e.g. via `avaje-recordbuilder`'s
|
|
||||||
`@RecordBuilder`, which generates `Target.builder(existingInstance)`), just map the
|
|
||||||
graph-sourced fields as usual and layer the extra field on afterwards:
|
|
||||||
|
|
||||||
```java
|
|
||||||
Driver base = mapper.map(cDriver);
|
|
||||||
Driver full = DriverBuilder.builder(base).fleets(fleets).build();
|
|
||||||
```
|
|
||||||
|
|
||||||
No generator changes needed - the mapped instance is simply the seed for the builder.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Large targets: builder-based construction and named variants
|
|
||||||
|
|
||||||
Two features aimed at large, builder-shaped target DTOs (typically OpenAPI-generated records
|
|
||||||
with a generated builder), where a positional constructor call is unwieldy and a single query
|
|
||||||
needs to populate the target in more than one shape.
|
|
||||||
|
|
||||||
### Builder-based construction (`builder = AUTO | ALWAYS | NEVER`)
|
|
||||||
|
|
||||||
If the target has a static no-arg `Target.builder()` factory returning a type with a fluent
|
|
||||||
(returns-itself) setter per property plus a `build()` method - the shape
|
|
||||||
`avaje-recordbuilder`'s `@RecordBuilder` generates - the generated mapper can construct the
|
|
||||||
target via `Target.builder().prop(x)....build()` instead of `new Target(a, b, c, ...)`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public record User(Long id, String name, String email, /* ... 21 more fields */) {
|
|
||||||
|
|
||||||
public static UserBuilder builder() {
|
|
||||||
return UserBuilder.builder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
@DtoMapping(source = CUser.class, target = User.class)
|
|
||||||
package org.example.dto;
|
|
||||||
```
|
|
||||||
|
|
||||||
By default (`builder = AUTO`), the generator auto-detects a matching builder and uses it only
|
|
||||||
once the target has more than 5 properties, falling back to a positional constructor for
|
|
||||||
smaller DTOs. Override explicitly either direction:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@DtoMapping(source = CUser.class, target = User.class, builder = DtoMapping.Builder.ALWAYS)
|
|
||||||
```
|
|
||||||
|
|
||||||
`builder = ALWAYS` is a codegen-time error if no matching builder shape is found; `builder =
|
|
||||||
NEVER` always uses a positional constructor even if a builder is detected. This applies
|
|
||||||
regardless of whether the target is hand-authored or foreign/generated - `@DtoMapping` is
|
|
||||||
already declared externally via `package-info.java`, so no annotation on the target itself is
|
|
||||||
needed either way.
|
|
||||||
|
|
||||||
### Named variants excluding nested paths (`name=`, `exclude=`)
|
|
||||||
|
|
||||||
The same `(source, target)` pair can be registered more than once - one base mapping (leaving
|
|
||||||
`name()` empty) plus any number of named variants, each excluding one or more nested
|
|
||||||
ToOne/ToMany properties:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@DtoMapping(source = CUser.class, target = User.class)
|
|
||||||
@DtoMapping(source = CUser.class, target = User.class, name = "noFleets", exclude = "fleets")
|
|
||||||
package org.example.dto;
|
|
||||||
```
|
|
||||||
|
|
||||||
Both variants are generated into the **same** mapper class (one class per target, not one per
|
|
||||||
variant) - the generated `noFleets()` accessor returns a single shared/cached `DtoMapper<CUser,
|
|
||||||
User>` view (not reconstructed per call), omitting `fleets` from both its mapped output (`null`
|
|
||||||
for a ToOne, `List.of()` for a ToMany) and its own `fetchGroup()`. Each excluded property is still
|
|
||||||
evaluated inline at its own declared field position internally (guarded by a boolean flag) - a
|
|
||||||
variant's exclusions never change the evaluation order of the DTO's other properties. Select it
|
|
||||||
with the `query.mapTo(Class, DtoMapper)` overload, which takes an already-resolved mapper instance
|
|
||||||
directly - no string-based lookup:
|
|
||||||
|
|
||||||
```java
|
|
||||||
UserMapper userMapper = new UserMapper();
|
|
||||||
|
|
||||||
// full shape, with fleets fetched/mapped
|
|
||||||
List<User> withFleets = DB.find(CUser.class)
|
|
||||||
.mapTo(User.class, userMapper) // or plain .mapTo(User.class)
|
|
||||||
.findList();
|
|
||||||
|
|
||||||
// bulk listing shape - fleets excluded from both the fetch spec and the output
|
|
||||||
List<User> noFleets = DB.find(CUser.class)
|
|
||||||
.mapTo(User.class, userMapper.noFleets())
|
|
||||||
.findList();
|
|
||||||
```
|
|
||||||
|
|
||||||
Only nested ToOne/ToMany properties can be excluded - a scalar or `@DtoRef` property can't be,
|
|
||||||
since there's no type-safe "absent" value for an arbitrary scalar type. Named variants are
|
|
||||||
scoped to independent, top-level query results only - unlike the base mapping, they don't
|
|
||||||
participate in `DtoMapContext` identity de-duplication when nested elsewhere in a graph, since a
|
|
||||||
variant is never intended to be nested inside another DTO's mapping.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `@DtoPath` — renamed or flattened properties
|
|
||||||
|
|
||||||
By default a DTO property is matched to the source entity property (or nested DTO
|
|
||||||
mapper) of the **same name**. `@DtoPath` overrides that, allowing a DTO property to be
|
|
||||||
renamed and/or flattened from a nested path using dot-notation:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class ContactDto {
|
|
||||||
private final long id;
|
|
||||||
private final String firstName;
|
|
||||||
private final String lastName;
|
|
||||||
|
|
||||||
@DtoPath("customer.billingAddress.city")
|
|
||||||
private final String customerCity; // flattened, 2 hops through customer
|
|
||||||
|
|
||||||
// constructor / getters ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The generated mapper reads the path with a null-guard at each hop and adds the
|
|
||||||
necessary joins to the fetch spec automatically:
|
|
||||||
|
|
||||||
```java
|
|
||||||
(s.getCustomer() == null ? null
|
|
||||||
: (s.getCustomer().getBillingAddress() == null ? null
|
|
||||||
: s.getCustomer().getBillingAddress().getCity()))
|
|
||||||
```
|
|
||||||
|
|
||||||
`@DtoPath` is purely a compile-time/codegen-time hint — the DTO class itself carries no
|
|
||||||
runtime dependency on the annotation.
|
|
||||||
|
|
||||||
### Fetch-path collisions are a compile-time error
|
|
||||||
|
|
||||||
A `@DtoPath` whose fetch path is identical to a nested `ToOne`/`ToMany` property's own
|
|
||||||
fetch path on the *same* DTO (e.g. a nested `customer` field alongside
|
|
||||||
`@DtoPath("customer.name")` — both resolve to fetch path `"customer"`) fails the build
|
|
||||||
with a clear error, rather than silently discarding one side's fetched properties:
|
|
||||||
|
|
||||||
```
|
|
||||||
error: @DtoPath property 'customerName' on FooDto resolves to fetch path 'customer',
|
|
||||||
which collides with the nested mapping already using that same fetch path - Ebean's
|
|
||||||
fetch spec can only carry one set of properties per path, so one silently discards
|
|
||||||
the other. Move 'customerName' onto the nested DTO type instead, or choose a
|
|
||||||
@DtoPath that reaches into a different, non-colliding path.
|
|
||||||
```
|
|
||||||
|
|
||||||
Fix it either way it suggests: move the property onto the nested DTO type, or choose a
|
|
||||||
`@DtoPath` that reaches a different path (as `customerCity` above does deliberately,
|
|
||||||
using a 3-segment path through `customer.billingAddress` rather than colliding with a
|
|
||||||
plain `customer` nested field).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `@DtoRef` — id-only back-references (breaking cycles)
|
|
||||||
|
|
||||||
The DTO graph derived from a set of DTO types must form a DAG — codegen fails if it
|
|
||||||
doesn't. `@DtoRef` is the explicit escape hatch for an intentional back-reference, e.g.
|
|
||||||
a `Contact` DTO referencing its parent `Customer` by id only, rather than re-embedding
|
|
||||||
a full `CustomerDto` (which would recreate the `Customer → Contact → Customer` cycle):
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class ContactDto {
|
|
||||||
private final long id;
|
|
||||||
|
|
||||||
@DtoRef
|
|
||||||
private final Long customerId; // id-only, no nested CustomerDto re-embedded
|
|
||||||
|
|
||||||
// constructor / getters ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The generated fetch spec adds the association to the **root** `select(...)` rather
|
|
||||||
than a nested `.fetch(...)` — this reads the foreign-key column directly off the base
|
|
||||||
table (no SQL join):
|
|
||||||
|
|
||||||
```java
|
|
||||||
this.fetchGroup = FetchGroup.of(ContactStats.class)
|
|
||||||
.select("customer,contactCount,engagementScore") // "customer" -> FK column, no join
|
|
||||||
.build();
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
(source.getCustomer() == null ? null : source.getCustomer().getId())
|
|
||||||
```
|
|
||||||
|
|
||||||
If the same association is *also* independently nested-fetched elsewhere on the DTO
|
|
||||||
(e.g. `ContactDto` has both a nested `customer` field **and** `@DtoRef Long
|
|
||||||
customerId`), the generator recognizes the association is already covered and doesn't
|
|
||||||
add a redundant/duplicate select — no join is added twice.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `@DtoConvert` — custom property conversion
|
|
||||||
|
|
||||||
Some properties need more than a plain getter copy — a scalar coercion (`short` to
|
|
||||||
`boolean`), an enum-to-`String` mapping, or a conversion needing a real dependency (e.g.
|
|
||||||
decrypting a value with a cipher). `@DtoConvert(value = ConverterType.class, method =
|
|
||||||
"name")` covers both, combinable with `@DtoPath` when the source value also needs a
|
|
||||||
path/rename override:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class ContactDto {
|
|
||||||
@DtoPath("status")
|
|
||||||
@DtoConvert(value = ContactConversions.class, method = "toActive")
|
|
||||||
private final boolean active; // Contact.status (Short) -> boolean
|
|
||||||
|
|
||||||
@DtoConvert(value = SecretCipher.class, method = "decode")
|
|
||||||
private final String secretCode; // decrypted via a registered SecretCipher
|
|
||||||
|
|
||||||
// constructor / getters ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The generator resolves the referenced method at codegen time and dispatches one of two
|
|
||||||
ways, purely based on whether it's `static`:
|
|
||||||
|
|
||||||
- **Static method** — inlined as a direct static call
|
|
||||||
(`ContactConversions.toActive(source.getStatus())`). No registration needed at all —
|
|
||||||
use this for common, reusable, dependency-free coercions.
|
|
||||||
- **Instance method** — the generated mapper resolves one shared instance via
|
|
||||||
`DtoConverterManager.get(SecretCipher.class)`, wired as a constructor
|
|
||||||
parameter/field (the same shape as nested-mapper constructor injection), then calls
|
|
||||||
`secretCipher.decode(source.getSecretCode())`. Use this when the conversion needs a
|
|
||||||
real dependency.
|
|
||||||
|
|
||||||
### Registering an instance-dispatch converter
|
|
||||||
|
|
||||||
`DtoConverterManager` is a small, deliberately-scoped static put/get bridge — register
|
|
||||||
an already-constructed converter instance (e.g. built by your DI container) **before**
|
|
||||||
building the `Database`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
AES256Cipher cipher = ...; // already DI-constructed
|
|
||||||
DtoConverterManager.put(SecretCipher.class, cipher::decrypt); // or a small adapter class
|
|
||||||
|
|
||||||
Database db = DatabaseFactory.create(...); // generated mappers resolve converters from here
|
|
||||||
```
|
|
||||||
|
|
||||||
If nothing is registered for a required type, `DtoConverterManager.get(...)` throws a
|
|
||||||
`PersistenceException` immediately — this happens as an eager field initializer on the
|
|
||||||
generated `EbeanDtoMapperRegister`, so a missing registration fails fast at `Database`
|
|
||||||
build time, not lazily on first `mapTo(...)` call.
|
|
||||||
|
|
||||||
> **Testing tip:** since `EbeanDtoMapperRegister`'s mapper fields are all constructed
|
|
||||||
> together when the `Database` starts, register converters via a `DatabaseConfigProvider`
|
|
||||||
> (a `ServiceLoader` hook that runs before the `Database` is built) rather than a test
|
|
||||||
> `@BeforeAll`, so registration always happens before *any* test triggers startup —
|
|
||||||
> regardless of which test class runs first.
|
|
||||||
|
|
||||||
## `@DtoMixin` — overlaying annotations onto a DTO you can't edit
|
|
||||||
|
|
||||||
Some DTOs are generated elsewhere (e.g. from an OpenAPI spec, regenerated on every
|
|
||||||
build) and can't be annotated directly. `@DtoMixin(Target.class)` overlays
|
|
||||||
`@DtoPath`/`@DtoRef`/`@DtoConvert` from a separate companion type instead — directly
|
|
||||||
mirroring avaje-jsonb's `@Json.MixIn` mechanism. Declare a companion interface (or
|
|
||||||
class) whose method names match the target DTO's property names:
|
|
||||||
|
|
||||||
```java
|
|
||||||
// ContactMixinDto itself carries no Ebean annotations at all
|
|
||||||
public class ContactMixinDto {
|
|
||||||
public ContactMixinDto(long id, String firstName, boolean active, String secretCode) { ... }
|
|
||||||
// getters ...
|
|
||||||
}
|
|
||||||
|
|
||||||
@DtoMixin(ContactMixinDto.class)
|
|
||||||
interface ContactMixinDtoMixin {
|
|
||||||
|
|
||||||
@DtoPath("status")
|
|
||||||
@DtoConvert(value = ContactConversions.class, method = "toActive")
|
|
||||||
boolean active();
|
|
||||||
|
|
||||||
@DtoConvert(value = SecretCipher.class, method = "decode")
|
|
||||||
String secretCode();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The processor matches each mixin method to the target's property by name and applies
|
|
||||||
whichever annotations are present as if they were declared on the target field itself.
|
|
||||||
The mixin type is never instantiated and carries no runtime footprint — it's purely a
|
|
||||||
compile-time/codegen-time hint.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Computed / aggregate properties via `@Entity @View`
|
|
||||||
|
|
||||||
There's no dedicated "formula on DTO" annotation (a narrower `@Formula2`-on-DTO
|
|
||||||
variant was explored and rejected — see
|
|
||||||
[dto-mapping-design.md](../dto-mapping-design.md) for the reasoning). Instead, model
|
|
||||||
the computed value as its own read-only entity using `@View`, then map that entity to a
|
|
||||||
plain DTO with the same `@DtoMapping` machinery described above. `@View(name = "...")`
|
|
||||||
here just points a second entity at an **existing** table — it does not create a new
|
|
||||||
database view or table.
|
|
||||||
|
|
||||||
### Worked example — computed column (`@Formula2`)
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Entity
|
|
||||||
@View(name = "contact") // reads the existing 'contact' table, no new DDL
|
|
||||||
public class ContactSummary {
|
|
||||||
@Id
|
|
||||||
private Long id;
|
|
||||||
private String firstName;
|
|
||||||
private String lastName;
|
|
||||||
|
|
||||||
@Formula2("concat(firstName, ' ', lastName)")
|
|
||||||
private String fullName;
|
|
||||||
|
|
||||||
// getters ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class ContactSummaryDto {
|
|
||||||
private final Long id;
|
|
||||||
private final String fullName;
|
|
||||||
// constructor / getters ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
@DtoMapping(source = ContactSummary.class, target = ContactSummaryDto.class)
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
List<ContactSummaryDto> summaries = DB.find(ContactSummary.class)
|
|
||||||
.mapTo(ContactSummaryDto.class)
|
|
||||||
.findList();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Worked example — group-by aggregation (`@Sum`/`@Aggregation`)
|
|
||||||
|
|
||||||
The same `@View`-on-base-table pattern applies to Ebean's `@Sum`/`@Aggregation`
|
|
||||||
group-by formulas — the Blaze-Persistence parallel is an `@EntityView` with
|
|
||||||
`@Mapping("SIZE(...)")`/`@Mapping("SUM(...)")` correlated mappings:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Entity
|
|
||||||
@View(name = "contact")
|
|
||||||
public class ContactStats {
|
|
||||||
@Id
|
|
||||||
private Long id; // required so @Aggregation("count(id)") has something to
|
|
||||||
// count; deliberately never selected/mapped - selecting it
|
|
||||||
// would defeat the aggregation (one row per contact
|
|
||||||
// instead of one row per customer)
|
|
||||||
@ManyToOne
|
|
||||||
private Customer customer;
|
|
||||||
|
|
||||||
@Aggregation("count(id)")
|
|
||||||
private Long contactCount;
|
|
||||||
|
|
||||||
@Sum
|
|
||||||
private Integer engagementScore;
|
|
||||||
|
|
||||||
// getters ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class ContactStatsDto {
|
|
||||||
@DtoRef
|
|
||||||
private final Long customerId; // also the implicit GROUP BY key
|
|
||||||
private final Long contactCount;
|
|
||||||
private final Integer engagementScore;
|
|
||||||
// constructor / getters ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Because `customerId` uses `@DtoRef`, the generated fetch spec is
|
|
||||||
`select("customer,contactCount,engagementScore")` with **no join** — the query groups
|
|
||||||
by the FK column directly:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
select t0.customer_id, count(t0.id), sum(t0.engagement_score)
|
|
||||||
from contact t0
|
|
||||||
group by t0.customer_id
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Performance notes
|
|
||||||
|
|
||||||
### Fail-fast, no accidental lazy loading
|
|
||||||
|
|
||||||
`mapTo(...)` forces `query.setUnmodifiable(true)` under the hood. If the mapper ever
|
|
||||||
needs a property that wasn't fetched, it throws `LazyInitialisationException`
|
|
||||||
immediately rather than silently issuing an extra query per row or returning `null`.
|
|
||||||
`InterceptReadOnly` (the unmodifiable-graph bean state) is also cheap — a `boolean[]
|
|
||||||
loaded` flag array plus a `frozen` flag, not a full second copy of bean state.
|
|
||||||
|
|
||||||
### One mapper instance per pair
|
|
||||||
|
|
||||||
Generated mappers are constructed once (in dependency order — a mapper with nested
|
|
||||||
mappers takes them as constructor params) and reused across every `mapTo(...)` call for
|
|
||||||
that pair, resolved and cached by `DtoMapperManager` keyed on `(sourceType, dtoType)`.
|
|
||||||
|
|
||||||
### `DtoMapContext` overhead only where it earns its keep
|
|
||||||
|
|
||||||
As shown above, the generator only involves `DtoMapContext` for mappers that can
|
|
||||||
actually be reached via more than one path in some graph (dedup) or that have nested
|
|
||||||
mappers of their own (need to thread the context down); a DTO that's only ever a
|
|
||||||
top-level query result skips it entirely.
|
|
||||||
|
|
||||||
### Fetch strategy and pagination carry over unchanged
|
|
||||||
|
|
||||||
Existing fetch-strategy control (`+query`/`+lazy`, `fetchQuery()`) and pagination
|
|
||||||
(including keyset pagination and `findPagedList()`) work the same whether the query
|
|
||||||
target is an entity graph or a `mapTo(...)` DTO graph — no special-casing needed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Which should I use?
|
|
||||||
|
|
||||||
- **`mapTo(Dto.class)`** — the target is a **nested** shape (has its own ToOne/ToMany
|
|
||||||
DTO fields) that should mirror part of the entity graph; you want the fetch spec
|
|
||||||
derived automatically and verified to match the DTO's declared shape.
|
|
||||||
- **`asDto(Dto.class)`** / `DB.findDto(...)` — the target is a **flat** row (report,
|
|
||||||
summary, native/vendor SQL); you're comfortable with runtime-checked column-to-bean
|
|
||||||
matching, or the SQL doesn't map cleanly to entity property paths at all.
|
|
||||||
- **Plain entity query** — the caller needs a real, persistable, mutable entity — not a
|
|
||||||
read-only projection.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reference
|
|
||||||
|
|
||||||
### Why `package-info.java`?
|
|
||||||
|
|
||||||
`@DtoMapping` is declared on a package (`ElementType.PACKAGE`), not the DTO or the
|
|
||||||
entity, because:
|
|
||||||
- the DTO type is often owned/generated elsewhere (e.g. from an OpenAPI spec) and
|
|
||||||
shouldn't need to be annotated with an internal persistence/entity type;
|
|
||||||
- one entity may be the source for several different DTOs (e.g. a summary vs. a detail
|
|
||||||
view), and the same entity/DTO pair may need registering from multiple consuming
|
|
||||||
modules.
|
|
||||||
|
|
||||||
### Annotations at a glance
|
|
||||||
|
|
||||||
| Annotation | Target | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `@DtoMapping(source=, target=)` | `package-info.java` | Registers an entity → DTO pair, triggers mapper generation |
|
|
||||||
| `@DtoMapping(..., builder=)` | `package-info.java` | `AUTO` (default, threshold-based) / `ALWAYS` / `NEVER` - builder-chain vs positional constructor |
|
|
||||||
| `@DtoMapping(..., name=, exclude=)` | `package-info.java` | Registers a named variant sharing the base mapping's generated class, excluding nested paths |
|
|
||||||
| `@DtoPath("a.b.c")` | DTO field/getter | Renamed and/or flattened multi-hop property mapping |
|
|
||||||
| `@DtoRef` | DTO field/getter | Id-only back-reference; breaks a cycle; root-selects the FK (no join) |
|
|
||||||
| `@DtoConvert(value=, method=)` | DTO field/getter | Custom scalar conversion - static (no registration) or instance (via `DtoConverterManager`) dispatch |
|
|
||||||
| `@DtoMixin(Target.class)` | Companion interface/class | Overlays `@DtoPath`/`@DtoRef`/`@DtoConvert` onto a DTO that can't be annotated directly |
|
|
||||||
|
|
||||||
### Parallels with other tools
|
|
||||||
|
|
||||||
If you're coming from another mapping library, here's the rough correspondence:
|
|
||||||
|
|
||||||
| Ebean | MapStruct | Blaze-Persistence |
|
|
||||||
|---|---|---|
|
|
||||||
| Generated `DtoMapper` per (source, DTO) pair | Generated `@Mapper` implementation | `@EntityView` (interface + runtime proxy) |
|
|
||||||
| `@DtoPath("a.b.c")` | `@Mapping(target = "x", source = "a.b.c")` | `@Mapping("a.b.c")` |
|
|
||||||
| `@DtoRef` | `@Context`/manual cycle-breaking (no dedicated annotation) | Sub-view referencing an id-only projection |
|
|
||||||
| `@DtoConvert(value=, method=)` | `@Mapping(qualifiedByName = "...")` / custom mapper methods | Custom converter/`@Mapping` expression |
|
|
||||||
| `@DtoMixin(Target.class)` | N/A (annotate the `@Mapper` interface's abstract methods instead) | N/A |
|
|
||||||
| `DtoMapContext` identity de-dup | Not built in (opt-in `@MappingTarget`/manual caching) | Built in (entity-view identity) |
|
|
||||||
| `@Entity @View` + `@Formula2`/`@Sum`/`@Aggregation` for computed DTO values | N/A (MapStruct doesn't touch SQL) | `@Mapping("SIZE(...)")` / `@Mapping("SUM(...)")` correlated mappings |
|
|
||||||
|
|
||||||
See [dto-mapping-design.md](../dto-mapping-design.md) for the full design rationale and
|
|
||||||
[dto-mapping-requirements.md](../dto-mapping-requirements.md) for the accepted/rejected
|
|
||||||
requirements this feature was scoped against (issue
|
|
||||||
[#2540](https://github.com/ebean-orm/ebean/issues/2540)).
|
|
||||||
@@ -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,603 +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
|
|
||||||
|
|
||||||
`asDto(...)` maps a **flat**, single-row result. If the target DTO itself needs nested
|
|
||||||
DTO fields (ToOne/ToMany) mirroring part of the entity graph, use
|
|
||||||
`mapTo(Dto.class)` instead — see
|
|
||||||
[Mapping entity graphs to DTOs](mapping-entity-graphs-to-dtos.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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
|
|
||||||
+39
-56
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>ebean-parent</artifactId>
|
<artifactId>ebean-parent</artifactId>
|
||||||
<groupId>io.ebean</groupId>
|
<groupId>io.ebean</groupId>
|
||||||
<version>18.3.0</version>
|
<version>13.23.0-jakarta</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<name>ebean api</name>
|
<name>ebean api</name>
|
||||||
@@ -26,16 +26,20 @@
|
|||||||
<version>1.0</version>
|
<version>1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Class retention Nonnull and Nullable annotations
|
||||||
|
to assist with IDE auto-completion with Ebean API
|
||||||
|
-->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jspecify</groupId>
|
<groupId>io.avaje</groupId>
|
||||||
<artifactId>jspecify</artifactId>
|
<artifactId>avaje-lang</artifactId>
|
||||||
<version>1.0.0</version>
|
<version>1.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.avaje</groupId>
|
<groupId>io.avaje</groupId>
|
||||||
<artifactId>avaje-config</artifactId>
|
<artifactId>avaje-config</artifactId>
|
||||||
<version>4.2</version>
|
<version>3.8</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -70,13 +74,15 @@
|
|||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Jackson core used internally by Ebean -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.avaje</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>avaje-json-core</artifactId>
|
<artifactId>jackson-core</artifactId>
|
||||||
<version>${avaje-json-core.version}</version>
|
<version>${jackson.version}</version>
|
||||||
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Jackson databind remains for ObjectMapper compatibility paths -->
|
<!-- provided scope for JsonNode support -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
@@ -84,6 +90,30 @@
|
|||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</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 -->
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.avaje</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>1.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -103,53 +133,6 @@
|
|||||||
</excludes>
|
</excludes>
|
||||||
</resource>
|
</resource>
|
||||||
</resources>
|
</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>
|
</build>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
|
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
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
|
* This also propagates MDC context from the current thread to the
|
||||||
* background task if defined.
|
* background task if defined.
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public interface BackgroundExecutor {
|
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;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
import org.jspecify.annotations.Nullable;
|
import io.avaje.lang.Nullable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ import java.util.Optional;
|
|||||||
*
|
*
|
||||||
* @see BeanRepository
|
* @see BeanRepository
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public abstract class BeanFinder<I,T> {
|
public abstract class BeanFinder<I,T> {
|
||||||
|
|
||||||
protected final Database database;
|
protected final Database database;
|
||||||
@@ -155,10 +155,4 @@ public abstract class BeanFinder<I,T> {
|
|||||||
return db().findNative(type, nativeSql);
|
return db().findNative(type, nativeSql);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a query using the ORM query language.
|
|
||||||
*/
|
|
||||||
protected Query<T> query(String ormQuery) {
|
|
||||||
return db().createQuery(type, ormQuery);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
import io.ebean.bean.EntityBean;
|
import io.ebean.bean.EntityBean;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -36,7 +36,7 @@ import java.util.Collection;
|
|||||||
* @param <I> The ID type
|
* @param <I> The ID type
|
||||||
* @param <T> The Bean type
|
* @param <T> The Bean type
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public abstract class BeanRepository<I, T> extends BeanFinder<I, T> {
|
public abstract class BeanRepository<I, T> extends BeanFinder<I, T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -89,7 +89,12 @@ public interface BeanState {
|
|||||||
* <p>
|
* <p>
|
||||||
* If a setter is called on a readOnly bean it will throw an exception.
|
* 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
|
* Advanced - Used to programmatically build a partially or fully loaded
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
import org.jspecify.annotations.Nullable;
|
import io.avaje.lang.Nullable;
|
||||||
import io.ebean.annotation.TxIsolation;
|
import io.ebean.annotation.TxIsolation;
|
||||||
import io.ebean.cache.ServerCacheManager;
|
import io.ebean.cache.ServerCacheManager;
|
||||||
import io.ebean.plugin.Property;
|
import io.ebean.plugin.Property;
|
||||||
@@ -57,7 +57,7 @@ import java.util.concurrent.Callable;
|
|||||||
*
|
*
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public final class DB {
|
public final class DB {
|
||||||
|
|
||||||
private static final DbContext context = DbContext.getInstance();
|
private static final DbContext context = DbContext.getInstance();
|
||||||
@@ -266,6 +266,55 @@ public final class DB {
|
|||||||
getDefault().register(transactionCallback);
|
getDefault().register(transactionCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated for removal migrate using try-with-resources and commit on the transaction itself.
|
||||||
|
* <p>
|
||||||
|
* Commit the current transaction.
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
|
public static void commitTransaction() {
|
||||||
|
getDefault().commitTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated for removal migrate to using try-with-resources and rollback on the transaction itself.
|
||||||
|
* <p>
|
||||||
|
* Rollback the current transaction.
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
|
public static void rollbackTransaction() {
|
||||||
|
getDefault().rollbackTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the current transaction has already been committed do nothing otherwise
|
||||||
|
* rollback the transaction.
|
||||||
|
* <p>
|
||||||
|
* It is preferable to use <em>try with resources</em> rather than this.
|
||||||
|
* <p>
|
||||||
|
* Useful to put in a finally block to ensure the transaction is ended, rather
|
||||||
|
* than a rollbackTransaction() in each catch block.
|
||||||
|
* <p>
|
||||||
|
* Code example:
|
||||||
|
*
|
||||||
|
* <pre>{@code
|
||||||
|
* DB.beginTransaction();
|
||||||
|
* try {
|
||||||
|
* // do some fetching and or persisting
|
||||||
|
*
|
||||||
|
* // commit at the end
|
||||||
|
* DB.commitTransaction();
|
||||||
|
*
|
||||||
|
* } finally {
|
||||||
|
* // if commit didn't occur then rollback the transaction
|
||||||
|
* DB.endTransaction();
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public static void endTransaction() {
|
||||||
|
getDefault().endTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark the current transaction as rollback only.
|
* Mark the current transaction as rollback only.
|
||||||
*/
|
*/
|
||||||
@@ -457,10 +506,6 @@ public final class DB {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as {@link #checkUniqueness(Object)} but with given transaction.
|
* Same as {@link #checkUniqueness(Object)} but with given transaction.
|
||||||
* <p>
|
|
||||||
* For control over query cache use and whether to skip the check when the bean's unique
|
|
||||||
* properties are unchanged, use {@link Database#checkUniqueness(Object, Transaction, boolean, boolean)}
|
|
||||||
* via {@link #getDefault()} instead.
|
|
||||||
*/
|
*/
|
||||||
public static Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
public static Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
||||||
return getDefault().checkUniqueness(bean, transaction);
|
return getDefault().checkUniqueness(bean, transaction);
|
||||||
@@ -609,7 +654,7 @@ public final class DB {
|
|||||||
* // find orders and their customers
|
* // find orders and their customers
|
||||||
* List<Order> list = DB.find(Order.class)
|
* List<Order> list = DB.find(Order.class)
|
||||||
* .fetch("customer")
|
* .fetch("customer")
|
||||||
* .orderBy("id")
|
* .order("id")
|
||||||
* .findList();
|
* .findList();
|
||||||
*
|
*
|
||||||
* // sort by customer name ascending, then by order shipDate
|
* // sort by customer name ascending, then by order shipDate
|
||||||
@@ -745,21 +790,6 @@ public final class DB {
|
|||||||
return getDefault().createUpdate(beanType, ormUpdate);
|
return getDefault().createUpdate(beanType, ormUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a named query.
|
|
||||||
* <p>
|
|
||||||
* For RawSql the named query is expected to be in ebean.xml.
|
|
||||||
*
|
|
||||||
* @param beanType The type of entity bean
|
|
||||||
* @param namedQuery The name of the query
|
|
||||||
* @param <T> The type of entity bean
|
|
||||||
* @return The query
|
|
||||||
*/
|
|
||||||
public static <T> Query<T> createNamedQuery(Class<T> beanType, String namedQuery) {
|
|
||||||
return getDefault().createNamedQuery(beanType, namedQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a query for a type of entity bean.
|
* Create a query for a type of entity bean.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -779,43 +809,6 @@ public final class DB {
|
|||||||
return getDefault().createQuery(beanType);
|
return getDefault().createQuery(beanType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the Ebean query language statement returning the query which can then
|
|
||||||
* be modified (add expressions, change order by clause, change maxRows, change
|
|
||||||
* fetch and select paths etc).
|
|
||||||
* <p>
|
|
||||||
* <h3>Example</h3>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* // Find order additionally fetching the customer, details and details.product name.
|
|
||||||
*
|
|
||||||
* String eql = "fetch customer fetch details fetch details.product (name) where id = :orderId ";
|
|
||||||
*
|
|
||||||
* Query<Order> query = DB.createQuery(Order.class, eql);
|
|
||||||
* query.setParameter("orderId", 2);
|
|
||||||
*
|
|
||||||
* Order order = query.findOne();
|
|
||||||
*
|
|
||||||
* // This is the same as:
|
|
||||||
*
|
|
||||||
* Order order = DB.find(Order.class)
|
|
||||||
* .fetch("customer")
|
|
||||||
* .fetch("details")
|
|
||||||
* .fetch("detail.product", "name")
|
|
||||||
* .setId(2)
|
|
||||||
* .findOne();
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* @param beanType The type of bean to fetch
|
|
||||||
* @param eql The Ebean query
|
|
||||||
* @param <T> The type of the entity bean
|
|
||||||
* @return The query with expressions defined as per the parsed query statement
|
|
||||||
*/
|
|
||||||
public static <T> Query<T> createQuery(Class<T> beanType, String eql) {
|
|
||||||
return getDefault().createQuery(beanType, eql);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a query for a type of entity bean.
|
* Create a query for a type of entity bean.
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
package io.ebean;
|
|
||||||
|
|
||||||
final class DInsertOptionsBuilder implements InsertOptions.Builder {
|
|
||||||
|
|
||||||
private Boolean getGeneratedKeys;
|
|
||||||
private boolean onConflictUpdate;
|
|
||||||
private boolean onConflictNothing;
|
|
||||||
private String constraint;
|
|
||||||
private String uniqueColumns;
|
|
||||||
private String updateSet;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InsertOptions.Builder onConflictNothing() {
|
|
||||||
this.onConflictNothing = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InsertOptions.Builder onConflictUpdate() {
|
|
||||||
this.onConflictUpdate = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InsertOptions.Builder constraint(String constraint) {
|
|
||||||
this.constraint = constraint;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InsertOptions.Builder uniqueColumns(String uniqueColumns) {
|
|
||||||
this.uniqueColumns = uniqueColumns;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InsertOptions.Builder updateSet(String updateSet) {
|
|
||||||
this.updateSet = updateSet;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InsertOptions.Builder getGeneratedKeys(boolean getGeneratedKeys) {
|
|
||||||
this.getGeneratedKeys = getGeneratedKeys;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InsertOptions build() {
|
|
||||||
return new Options(constraint, uniqueColumns, updateSet, onConflictUpdate, onConflictNothing, getGeneratedKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class Options implements InsertOptions {
|
|
||||||
|
|
||||||
private static final String UPDATE = "U";
|
|
||||||
private static final String NOTHING = "N";
|
|
||||||
private static final String NORMAL = "_";
|
|
||||||
private final String key;
|
|
||||||
private final Boolean getGeneratedKeys;
|
|
||||||
private final String constraint;
|
|
||||||
private final String uniqueColumns;
|
|
||||||
private final String updateSet;
|
|
||||||
|
|
||||||
Options(String constraint, String uniqueColumns, String updateSet, boolean onConflictUpdate, boolean onConflictNothing, Boolean getGeneratedKeys) {
|
|
||||||
this.constraint = constraint;
|
|
||||||
this.uniqueColumns = uniqueColumns;
|
|
||||||
this.updateSet = updateSet;
|
|
||||||
this.getGeneratedKeys = getGeneratedKeys;
|
|
||||||
this.key = (onConflictUpdate ? UPDATE : onConflictNothing ? NOTHING : NORMAL)
|
|
||||||
+ '+' + plus(constraint)
|
|
||||||
+ '+' + plus(uniqueColumns)
|
|
||||||
+ '+' + plus(updateSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String plus(String val) {
|
|
||||||
return val == null ? "" : val;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String key() {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String constraint() {
|
|
||||||
return constraint;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String uniqueColumns() {
|
|
||||||
return uniqueColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String updateSet() {
|
|
||||||
return updateSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Boolean getGetGeneratedKeys() {
|
|
||||||
return getGeneratedKeys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package io.ebean;
|
|||||||
import jakarta.persistence.PersistenceException;
|
import jakarta.persistence.PersistenceException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown when a foreign key constraint is enforced or a field is too large.
|
* Thrown when a foreign key constraint is enforced.
|
||||||
*/
|
*/
|
||||||
public class DataIntegrityException extends PersistenceException {
|
public class DataIntegrityException extends PersistenceException {
|
||||||
private static final long serialVersionUID = -6740171949170180970L;
|
private static final long serialVersionUID = -6740171949170180970L;
|
||||||
@@ -14,11 +14,4 @@ public class DataIntegrityException extends PersistenceException {
|
|||||||
public DataIntegrityException(String message, Throwable cause) {
|
public DataIntegrityException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create with message only.
|
|
||||||
*/
|
|
||||||
public DataIntegrityException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
import org.jspecify.annotations.Nullable;
|
import io.avaje.lang.Nullable;
|
||||||
import io.ebean.annotation.Platform;
|
import io.ebean.annotation.Platform;
|
||||||
import io.ebean.annotation.TxIsolation;
|
import io.ebean.annotation.TxIsolation;
|
||||||
import io.ebean.cache.ServerCacheManager;
|
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.
|
* 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>
|
* <h5>Registration with the DB singleton</h5>
|
||||||
* <p>
|
* <p>
|
||||||
* When a Database instance is created it can be registered with the {@link DB}
|
* When a Database instance is created it can be registered with the DB
|
||||||
* singleton (see {@link DatabaseBuilder#register(boolean)}). The {@link DB}
|
* singleton (see {@link DatabaseConfig#setRegister(boolean)}). The DB
|
||||||
* singleton is essentially a map of {@link Database}'s that have been registered
|
* singleton is essentially a map of Database's that have been registered
|
||||||
* with it.
|
* with it.
|
||||||
* <p>
|
* <p>
|
||||||
* The Database can then be retrieved later via {@link DB#byName(String)}.
|
* 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>
|
* <h5>The 'default' Database</h5>
|
||||||
* <p>
|
* <p>
|
||||||
* One Database can be designated as the 'default' or 'primary' Database
|
* 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
|
* such as {@link DB#find(Class)} etc are actually just a convenient way to
|
||||||
* call methods on the 'default/primary' Database.
|
* 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(DatabaseConfig)} or they
|
||||||
|
* can be automatically constructed on demand using configuration information in
|
||||||
|
* the application.properties file.
|
||||||
*
|
*
|
||||||
* <h5>Example: Get a Database</h5>
|
* <h5>Example: Get a Database</h5>
|
||||||
* <pre>{@code
|
* <pre>{@code
|
||||||
@@ -81,32 +80,12 @@ import java.util.concurrent.Callable;
|
|||||||
* method. Example: a single thread requires more than one transaction.
|
* method. Example: a single thread requires more than one transaction.
|
||||||
*
|
*
|
||||||
* @see DB
|
* @see DB
|
||||||
* @see DatabaseBuilder
|
|
||||||
* @see DatabaseFactory
|
* @see DatabaseFactory
|
||||||
* @see DatabaseConfig
|
* @see DatabaseConfig
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public interface Database {
|
public interface Database {
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a new database builder.
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* // build the 'default' database using configuration
|
|
||||||
* // from application.properties / application.yaml
|
|
||||||
*
|
|
||||||
* Database db = Database.builder()
|
|
||||||
* .name("db")
|
|
||||||
* .loadFromProperties()
|
|
||||||
* .build();
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("removal")
|
|
||||||
static DatabaseBuilder builder() {
|
|
||||||
return new DatabaseConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shutdown the Database instance.
|
* Shutdown the Database instance.
|
||||||
*/
|
*/
|
||||||
@@ -141,7 +120,6 @@ public interface Database {
|
|||||||
/**
|
/**
|
||||||
* Return the associated read only DataSource for this Database instance (can be null).
|
* Return the associated read only DataSource for this Database instance (can be null).
|
||||||
*/
|
*/
|
||||||
@Nullable
|
|
||||||
DataSource readOnlyDataSource();
|
DataSource readOnlyDataSource();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -247,18 +225,6 @@ public interface Database {
|
|||||||
*/
|
*/
|
||||||
<T> UpdateQuery<T> update(Class<T> beanType);
|
<T> UpdateQuery<T> update(Class<T> beanType);
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a named query.
|
|
||||||
* <p>
|
|
||||||
* For RawSql the named query is expected to be in ebean.xml.
|
|
||||||
*
|
|
||||||
* @param beanType The type of entity bean
|
|
||||||
* @param namedQuery The name of the query
|
|
||||||
* @param <T> The type of entity bean
|
|
||||||
* @return The query
|
|
||||||
*/
|
|
||||||
<T> Query<T> createNamedQuery(Class<T> beanType, String namedQuery);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a query for an entity bean and synonym for {@link #find(Class)}.
|
* Create a query for an entity bean and synonym for {@link #find(Class)}.
|
||||||
*
|
*
|
||||||
@@ -266,41 +232,6 @@ public interface Database {
|
|||||||
*/
|
*/
|
||||||
<T> Query<T> createQuery(Class<T> beanType);
|
<T> Query<T> createQuery(Class<T> beanType);
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the Ebean query language statement returning the query which can then
|
|
||||||
* be modified (add expressions, change order by clause, change maxRows, change
|
|
||||||
* fetch and select paths etc).
|
|
||||||
* <p>
|
|
||||||
* <h3>Example</h3>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* // Find order additionally fetching the customer, details and details.product name.
|
|
||||||
*
|
|
||||||
* String ormQuery = "fetch customer fetch details fetch details.product (name) where id = :orderId ";
|
|
||||||
*
|
|
||||||
* Query<Order> query = DB.createQuery(Order.class, ormQuery);
|
|
||||||
* query.setParameter("orderId", 2);
|
|
||||||
*
|
|
||||||
* Order order = query.findOne();
|
|
||||||
*
|
|
||||||
* // This is the same as:
|
|
||||||
*
|
|
||||||
* Order order = DB.find(Order.class)
|
|
||||||
* .fetch("customer")
|
|
||||||
* .fetch("details")
|
|
||||||
* .fetch("detail.product", "name")
|
|
||||||
* .setId(2)
|
|
||||||
* .findOne();
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* @param beanType The type of bean to fetch
|
|
||||||
* @param ormQuery The Ebean ORM query
|
|
||||||
* @param <T> The type of the entity bean
|
|
||||||
* @return The query with expressions defined as per the parsed query statement
|
|
||||||
*/
|
|
||||||
<T> Query<T> createQuery(Class<T> beanType, String ormQuery);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a query for a type of entity bean.
|
* Create a query for a type of entity bean.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -405,7 +336,7 @@ public interface Database {
|
|||||||
* // find orders and their customers
|
* // find orders and their customers
|
||||||
* List<Order> list = database.find(Order.class)
|
* List<Order> list = database.find(Order.class)
|
||||||
* .fetch("customer")
|
* .fetch("customer")
|
||||||
* .orderBy("id")
|
* .order("id")
|
||||||
* .findList();
|
* .findList();
|
||||||
*
|
*
|
||||||
* // sort by customer name ascending, then by order shipDate
|
* // sort by customer name ascending, then by order shipDate
|
||||||
@@ -464,18 +395,6 @@ public interface Database {
|
|||||||
*/
|
*/
|
||||||
<T> DtoQuery<T> findDto(Class<T> dtoType, String sql);
|
<T> DtoQuery<T> findDto(Class<T> dtoType, String sql);
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a named Query for DTO beans.
|
|
||||||
* <p>
|
|
||||||
* DTO beans are just normal bean like classes with public constructor(s) and setters.
|
|
||||||
* They do not need to be registered with DB before use.
|
|
||||||
*
|
|
||||||
* @param dtoType The type of the DTO bean the rows will be mapped into.
|
|
||||||
* @param namedQuery The name of the query
|
|
||||||
* @param <T> The type of the DTO bean.
|
|
||||||
*/
|
|
||||||
<T> DtoQuery<T> createNamedDtoQuery(Class<T> dtoType, String namedQuery);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look to execute a native sql query that does not return beans but instead
|
* Look to execute a native sql query that does not return beans but instead
|
||||||
* returns SqlRow or direct access to ResultSet.
|
* returns SqlRow or direct access to ResultSet.
|
||||||
@@ -671,6 +590,49 @@ public interface Database {
|
|||||||
*/
|
*/
|
||||||
void flush();
|
void flush();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated for removal migrate using try-with-resources and commit on the transaction itself.
|
||||||
|
* <p>
|
||||||
|
* Commit the current transaction.
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
|
void commitTransaction();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated for removal migrate to using try-with-resources and rollback on the transaction itself.
|
||||||
|
* <p>
|
||||||
|
* Rollback the current transaction.
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
|
void rollbackTransaction();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the current transaction has already been committed do nothing otherwise
|
||||||
|
* rollback the transaction.
|
||||||
|
* <p>
|
||||||
|
* Useful to put in a finally block to ensure the transaction is ended, rather
|
||||||
|
* than a rollbackTransaction() in each catch block.
|
||||||
|
* <p>
|
||||||
|
* Code example:
|
||||||
|
* <p>
|
||||||
|
* <pre>{@code
|
||||||
|
*
|
||||||
|
* database.beginTransaction();
|
||||||
|
* try {
|
||||||
|
* // do some fetching and or persisting ...
|
||||||
|
*
|
||||||
|
* // commit at the end
|
||||||
|
* database.commitTransaction();
|
||||||
|
*
|
||||||
|
* } finally {
|
||||||
|
* // if commit didn't occur then rollback the transaction
|
||||||
|
* database.endTransaction();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
void endTransaction();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh the values of a bean.
|
* Refresh the values of a bean.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -777,6 +739,18 @@ public interface Database {
|
|||||||
*/
|
*/
|
||||||
<T> T reference(Class<T> beanType, Object id);
|
<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.
|
* Either Insert or Update the bean depending on its state.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -1078,21 +1052,12 @@ public interface Database {
|
|||||||
* @param bean The entity bean to check uniqueness on
|
* @param bean The entity bean to check uniqueness on
|
||||||
* @return a set of Properties if constraint validation was detected or empty list.
|
* @return a set of Properties if constraint validation was detected or empty list.
|
||||||
*/
|
*/
|
||||||
default Set<Property> checkUniqueness(Object bean) {
|
Set<Property> checkUniqueness(Object bean);
|
||||||
return checkUniqueness(bean, null, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as {@link #checkUniqueness(Object)}. but with given transaction.
|
* Same as {@link #checkUniqueness(Object)}. but with given transaction.
|
||||||
*/
|
*/
|
||||||
default Set<Property> checkUniqueness(Object bean, Transaction transaction) {
|
Set<Property> checkUniqueness(Object bean, Transaction transaction);
|
||||||
return checkUniqueness(bean, transaction, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as {@link #checkUniqueness(Object)}. but with given transaction and extended search options.
|
|
||||||
*/
|
|
||||||
Set<Property> checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the entity bean as dirty.
|
* Marks the entity bean as dirty.
|
||||||
@@ -1186,53 +1151,22 @@ public interface Database {
|
|||||||
*/
|
*/
|
||||||
void insert(Object bean);
|
void insert(Object bean);
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert the bean with options (ON CONFLICT DO UPDATE | DO NOTHING).
|
|
||||||
* <p>
|
|
||||||
* Currently, this is limited to use with Postgres only,
|
|
||||||
* <p>
|
|
||||||
* When using this ebean will look to determine the unique columns by looking at
|
|
||||||
* the mapping like {@code @Column(unique=true} and {@code @Index(unique=true}.
|
|
||||||
*/
|
|
||||||
void insert(Object bean, InsertOptions insertOptions);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert the bean with a transaction.
|
* Insert the bean with a transaction.
|
||||||
*/
|
*/
|
||||||
void insert(Object bean, Transaction transaction);
|
void insert(Object bean, Transaction transaction);
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert the beans with options (ON CONFLICT DO UPDATE | DO NOTHING) and transaction.
|
|
||||||
* <p>
|
|
||||||
* Currently, this is limited to use with Postgres only,
|
|
||||||
*/
|
|
||||||
void insert(Object bean, InsertOptions insertOptions, Transaction transaction);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a collection of beans. If there is no current transaction one is created and used to
|
* Insert a collection of beans. If there is no current transaction one is created and used to
|
||||||
* insert all the beans in the collection.
|
* insert all the beans in the collection.
|
||||||
*/
|
*/
|
||||||
void insertAll(Collection<?> beans);
|
void insertAll(Collection<?> beans);
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert the beans with options - typically ON CONFLICT DO UPDATE | DO NOTHING.
|
|
||||||
* <p>
|
|
||||||
* Currently, this is limited to use with Postgres only,
|
|
||||||
*/
|
|
||||||
void insertAll(Collection<?> beans, InsertOptions options);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a collection of beans with an explicit transaction.
|
* Insert a collection of beans with an explicit transaction.
|
||||||
*/
|
*/
|
||||||
void insertAll(Collection<?> beans, Transaction transaction);
|
void insertAll(Collection<?> beans, Transaction transaction);
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert the beans with options (ON CONFLICT DO UPDATE | DO NOTHING) and transaction.
|
|
||||||
* <p>
|
|
||||||
* Currently, this is limited to use with Postgres only,
|
|
||||||
*/
|
|
||||||
void insertAll(Collection<?> beans, InsertOptions options, Transaction transaction);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute explicitly passing a transaction.
|
* Execute explicitly passing a transaction.
|
||||||
*/
|
*/
|
||||||
@@ -1387,109 +1321,6 @@ public interface Database {
|
|||||||
*/
|
*/
|
||||||
ScriptRunner script();
|
ScriptRunner script();
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the Document store.
|
|
||||||
*/
|
|
||||||
DocumentStore docStore();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish a single bean given its type and id returning the resulting live bean.
|
|
||||||
* <p>
|
|
||||||
* The values are published from the draft to the live bean.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the entity bean
|
|
||||||
* @param beanType the type of the entity bean
|
|
||||||
* @param id the id of the entity bean
|
|
||||||
* @param transaction the transaction the publish process should use (can be null)
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
<T> T publish(Class<T> beanType, Object id, Transaction transaction);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish a single bean given its type and id returning the resulting live bean.
|
|
||||||
* This will use the current transaction or create one if required.
|
|
||||||
* <p>
|
|
||||||
* The values are published from the draft to the live bean.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the entity bean
|
|
||||||
* @param beanType the type of the entity bean
|
|
||||||
* @param id the id of the entity bean
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
<T> T publish(Class<T> beanType, Object id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish the beans that match the query returning the resulting published beans.
|
|
||||||
* <p>
|
|
||||||
* The values are published from the draft beans to the live beans.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the entity bean
|
|
||||||
* @param query the query used to select the draft beans to publish
|
|
||||||
* @param transaction the transaction the publish process should use (can be null)
|
|
||||||
*/
|
|
||||||
<T> List<T> publish(Query<T> query, Transaction transaction);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish the beans that match the query returning the resulting published beans.
|
|
||||||
* This will use the current transaction or create one if required.
|
|
||||||
* <p>
|
|
||||||
* The values are published from the draft beans to the live beans.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the entity bean
|
|
||||||
* @param query the query used to select the draft beans to publish
|
|
||||||
*/
|
|
||||||
<T> List<T> publish(Query<T> query);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the draft bean back to the live state.
|
|
||||||
* <p>
|
|
||||||
* The values from the live beans are set back to the draft bean and the
|
|
||||||
* <code>@DraftDirty</code> and <code>@DraftReset</code> properties are reset.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the entity bean
|
|
||||||
* @param beanType the type of the entity bean
|
|
||||||
* @param id the id of the entity bean to restore
|
|
||||||
* @param transaction the transaction the restore process should use (can be null)
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
<T> T draftRestore(Class<T> beanType, Object id, Transaction transaction);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the draft bean back to the live state.
|
|
||||||
* <p>
|
|
||||||
* The values from the live beans are set back to the draft bean and the
|
|
||||||
* <code>@DraftDirty</code> and <code>@DraftReset</code> properties are reset.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the entity bean
|
|
||||||
* @param beanType the type of the entity bean
|
|
||||||
* @param id the id of the entity bean to restore
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
<T> T draftRestore(Class<T> beanType, Object id);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the draft beans matching the query back to the live state.
|
|
||||||
* <p>
|
|
||||||
* The values from the live beans are set back to the draft bean and the
|
|
||||||
* <code>@DraftDirty</code> and <code>@DraftReset</code> properties are reset.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the entity bean
|
|
||||||
* @param query the query used to select the draft beans to restore
|
|
||||||
* @param transaction the transaction the restore process should use (can be null)
|
|
||||||
*/
|
|
||||||
<T> List<T> draftRestore(Query<T> query, Transaction transaction);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the draft beans matching the query back to the live state.
|
|
||||||
* <p>
|
|
||||||
* The values from the live beans are set back to the draft bean and the
|
|
||||||
* <code>@DraftDirty</code> and <code>@DraftReset</code> properties are reset.
|
|
||||||
*
|
|
||||||
* @param <T> the type of the entity bean
|
|
||||||
* @param query the query used to select the draft beans to restore
|
|
||||||
*/
|
|
||||||
<T> List<T> draftRestore(Query<T> query);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the set of properties/paths that are unknown (do not map to known properties or paths).
|
* Returns the set of properties/paths that are unknown (do not map to known properties or paths).
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,29 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import io.ebean.config.ContainerConfig;
|
import io.ebean.config.ContainerConfig;
|
||||||
|
import io.ebean.config.DatabaseConfig;
|
||||||
import io.ebean.service.SpiContainer;
|
import io.ebean.service.SpiContainer;
|
||||||
import io.ebean.service.SpiContainerFactory;
|
import io.ebean.service.SpiContainerFactory;
|
||||||
import jakarta.persistence.PersistenceException;
|
|
||||||
|
|
||||||
|
import jakarta.persistence.PersistenceException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Low-level factory for creating {@link Database} instances.
|
* Creates Database instances.
|
||||||
* <p>
|
* <p>
|
||||||
* Most applications should prefer {@link Database#builder()} together with {@link DatabaseBuilder#build()}.
|
* This uses either DatabaseConfig or properties in the application.properties file to
|
||||||
* This factory remains for legacy creation entry points plus container lifecycle methods.
|
* configure and create a Database instance.
|
||||||
* <p>
|
* <p>
|
||||||
* The Database instance can either be registered with the {@link DB} singleton or
|
* The Database instance can either be registered with the DB singleton or
|
||||||
* not. The {@link DB} singleton effectively holds a map of {@link Database} by name.
|
* not. The DB singleton effectively holds a map of Database by a name.
|
||||||
* If the Database is registered with the {@link DB} singleton you can retrieve it
|
* If the Database is registered with the DB singleton you can retrieve it
|
||||||
* later via {@link DB#byName(String)}.
|
* later via {@link DB#byName(String)}.
|
||||||
* <p>
|
* <p>
|
||||||
* One Database can be nominated as the 'default/primary' Database. Many
|
* 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.
|
* convenient way of using the 'default/primary' Database.
|
||||||
*/
|
*/
|
||||||
public final class DatabaseFactory {
|
public final class DatabaseFactory {
|
||||||
@@ -36,8 +40,7 @@ public final class DatabaseFactory {
|
|||||||
* Initialise the container with clustering configuration.
|
* Initialise the container with clustering configuration.
|
||||||
* <p>
|
* <p>
|
||||||
* Call this prior to creating any Database instances or alternatively set the
|
* Call this prior to creating any Database instances or alternatively set the
|
||||||
* {@link ContainerConfig} on the first {@link DatabaseBuilder} via
|
* ContainerConfig on the DatabaseConfig when creating the first Database instance.
|
||||||
* {@link DatabaseBuilder#containerConfig(ContainerConfig)}.
|
|
||||||
*/
|
*/
|
||||||
public static void initialiseContainer(ContainerConfig containerConfig) {
|
public static void initialiseContainer(ContainerConfig containerConfig) {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
@@ -49,11 +52,8 @@ public final class DatabaseFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create using configuration loaded from properties for the given database name.
|
* Create using properties to configure the database.
|
||||||
*
|
|
||||||
* @deprecated migrate to {@code Database.builder().name(name).loadFromProperties().build()}.
|
|
||||||
*/
|
*/
|
||||||
@Deprecated
|
|
||||||
public static Database create(String name) {
|
public static Database create(String name) {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
@@ -64,35 +64,33 @@ 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(DatabaseConfig config) {
|
||||||
public static Database create(DatabaseBuilder builder) {
|
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
var config = builder.settings();
|
if (config.getName() == null) {
|
||||||
var name = config.getName();
|
|
||||||
if (name == null) {
|
|
||||||
throw new PersistenceException("The name is null (it is required)");
|
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);
|
|
||||||
if (DbContext.getInstance().contains(name)) {
|
|
||||||
throw new IllegalStateException("A Database with name [" + name + "] is already registered."
|
|
||||||
+ " Use a unique DatabaseConfig name, or set DatabaseConfig.setRegister(false)"
|
|
||||||
+ " if this Database instance is not intended to be registered/looked up by name.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Database server = createInternal(config);
|
Database server = createInternal(config);
|
||||||
if (config.isRegister()) {
|
if (config.isRegister()) {
|
||||||
if (config.isDefaultServer()) {
|
if (config.isDefaultServer()) {
|
||||||
if (defaultServerName != null && !defaultServerName.equals(name)) {
|
if (defaultServerName != null && !defaultServerName.equals(config.getName())) {
|
||||||
throw new IllegalStateException("Registering [" + name + "] as the default server but [" + defaultServerName + "] is already registered as the default");
|
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());
|
DbContext.getInstance().register(server, config.isDefaultServer());
|
||||||
}
|
}
|
||||||
return server;
|
return server;
|
||||||
@@ -102,10 +100,9 @@ public final class DatabaseFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create using the {@link DatabaseBuilder}, additionally specifying a classLoader to use as the
|
* Create using the DatabaseConfig additionally specifying a classLoader to use as the context class loader.
|
||||||
* context class loader.
|
|
||||||
*/
|
*/
|
||||||
public static Database createWithContextClassLoader(DatabaseBuilder config, ClassLoader classLoader) {
|
public static Database createWithContextClassLoader(DatabaseConfig config, ClassLoader classLoader) {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
ClassLoader currentContextLoader = Thread.currentThread().getContextClassLoader();
|
ClassLoader currentContextLoader = Thread.currentThread().getContextClassLoader();
|
||||||
@@ -121,24 +118,6 @@ public final class DatabaseFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the registration of this Database.
|
|
||||||
* <p>
|
|
||||||
* This is invoked when a Database is shutdown so that its registered name
|
|
||||||
* becomes available again for a subsequently created Database with the same name.
|
|
||||||
*/
|
|
||||||
public static void unregister(Database server) {
|
|
||||||
lock.lock();
|
|
||||||
try {
|
|
||||||
DbContext.getInstance().deregister(server);
|
|
||||||
if (server.name().equals(defaultServerName)) {
|
|
||||||
defaultServerName = null;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shutdown gracefully all Database instances cleaning up any resources as required.
|
* Shutdown gracefully all Database instances cleaning up any resources as required.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -153,7 +132,7 @@ public final class DatabaseFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Database createInternal(DatabaseBuilder.Settings config) {
|
private static Database createInternal(DatabaseConfig config) {
|
||||||
return container(config.getContainerConfig()).createServer(config);
|
return container(config.getContainerConfig()).createServer(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,9 +146,12 @@ public final class DatabaseFactory {
|
|||||||
if (container != null) {
|
if (container != null) {
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (containerConfig == null) {
|
if (containerConfig == null) {
|
||||||
// effectively load configuration from ebean.properties
|
// effectively load configuration from ebean.properties
|
||||||
|
Properties properties = DbPrimary.getProperties();
|
||||||
containerConfig = new ContainerConfig();
|
containerConfig = new ContainerConfig();
|
||||||
|
containerConfig.loadFromProperties(properties);
|
||||||
}
|
}
|
||||||
container = createContainer(containerConfig);
|
container = createContainer(containerConfig);
|
||||||
return container;
|
return container;
|
||||||
@@ -178,11 +160,11 @@ public final class DatabaseFactory {
|
|||||||
/**
|
/**
|
||||||
* Create the container instance using the configuration.
|
* Create the container instance using the configuration.
|
||||||
*/
|
*/
|
||||||
private static SpiContainer createContainer(ContainerConfig containerConfig) {
|
protected static SpiContainer createContainer(ContainerConfig containerConfig) {
|
||||||
SpiContainerFactory factory = XBootstrapService.containerFactory();
|
Iterator<SpiContainerFactory> factories = ServiceLoader.load(SpiContainerFactory.class).iterator();
|
||||||
if (factory == null) {
|
if (factories.hasNext()) {
|
||||||
throw new IllegalStateException("Service loader didn't find a SpiContainerFactory?");
|
return factories.next().create(containerConfig);
|
||||||
}
|
}
|
||||||
return factory.create(containerConfig);
|
throw new IllegalStateException("Service loader didn't find a SpiContainerFactory?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import io.ebean.config.BeanNotEnhancedException;
|
|||||||
import io.ebean.datasource.DataSourceConfigurationException;
|
import io.ebean.datasource.DataSourceConfigurationException;
|
||||||
|
|
||||||
import jakarta.persistence.PersistenceException;
|
import jakarta.persistence.PersistenceException;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
@@ -76,10 +75,6 @@ final class DbContext {
|
|||||||
return defaultDatabase;
|
return defaultDatabase;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean contains(String name) {
|
|
||||||
return concMap.containsKey(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the database by name.
|
* Return the database by name.
|
||||||
*/
|
*/
|
||||||
@@ -97,7 +92,6 @@ final class DbContext {
|
|||||||
/**
|
/**
|
||||||
* Read, create and put of Databases.
|
* Read, create and put of Databases.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
private Database getWithCreate(String name) {
|
private Database getWithCreate(String name) {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
@@ -120,27 +114,6 @@ final class DbContext {
|
|||||||
registerWithName(server.name(), server, isDefault);
|
registerWithName(server.name(), server, isDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the registration for this Database (typically on shutdown) so that
|
|
||||||
* its name becomes available again for a subsequently created Database.
|
|
||||||
* <p>
|
|
||||||
* Only removes the registration if it currently maps to this exact instance
|
|
||||||
* (avoids removing a different Database subsequently registered with the same name).
|
|
||||||
*/
|
|
||||||
void deregister(Database server) {
|
|
||||||
lock.lock();
|
|
||||||
try {
|
|
||||||
String name = server.name();
|
|
||||||
concMap.remove(name, server);
|
|
||||||
syncMap.remove(name, server);
|
|
||||||
if (defaultDatabase == server) {
|
|
||||||
defaultDatabase = null;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void registerWithName(String name, Database server, boolean isDefault) {
|
private void registerWithName(String name, Database server, boolean isDefault) {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -44,12 +44,25 @@ final class DbPrimary {
|
|||||||
* Return the default database name.
|
* Return the default database name.
|
||||||
*/
|
*/
|
||||||
static String getDefaultServerName() {
|
static String getDefaultServerName() {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
getProperties();
|
||||||
|
return defaultServerName;
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the default configuration Properties.
|
||||||
|
*/
|
||||||
|
static Properties getProperties() {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
if (defaultServerName == null) {
|
if (defaultServerName == null) {
|
||||||
defaultServerName = determineDefaultServerName();
|
defaultServerName = determineDefaultServerName();
|
||||||
}
|
}
|
||||||
return defaultServerName;
|
return Config.asProperties();
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
package io.ebean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bean holding the details to update the document store.
|
|
||||||
*/
|
|
||||||
public final class DocStoreQueueEntry {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action to either update or delete a document from the index.
|
|
||||||
*/
|
|
||||||
public enum Action {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action is to update a document in the doc store.
|
|
||||||
*/
|
|
||||||
INDEX(1),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action is to delete a document from the doc store..
|
|
||||||
*/
|
|
||||||
DELETE(2),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An update is required based on a change to a nested/embedded object at a given path.
|
|
||||||
*/
|
|
||||||
NESTED(3);
|
|
||||||
|
|
||||||
int value;
|
|
||||||
|
|
||||||
Action(int value) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the value associated with this action type.
|
|
||||||
*/
|
|
||||||
public int getValue() {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Action type;
|
|
||||||
|
|
||||||
private final String queueId;
|
|
||||||
|
|
||||||
private final String path;
|
|
||||||
|
|
||||||
private final Object beanId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct for an INDEX or DELETE action.
|
|
||||||
*/
|
|
||||||
public DocStoreQueueEntry(Action type, String queueId, Object beanId) {
|
|
||||||
this(type, queueId, null, beanId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct for an NESTED/embedded path invalidation action.
|
|
||||||
*/
|
|
||||||
public DocStoreQueueEntry(Action type, String queueId, String path, Object beanId) {
|
|
||||||
this.type = type;
|
|
||||||
this.queueId = queueId;
|
|
||||||
this.path = path;
|
|
||||||
this.beanId = beanId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the event type.
|
|
||||||
*/
|
|
||||||
public Action getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the associate queueId.
|
|
||||||
*/
|
|
||||||
public String getQueueId() {
|
|
||||||
return queueId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the path if this is a nested update.
|
|
||||||
*/
|
|
||||||
public String getPath() {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the bean id (which matches the document id).
|
|
||||||
*/
|
|
||||||
public Object getBeanId() {
|
|
||||||
return beanId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
package io.ebean;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
import io.ebean.docstore.DocQueryContext;
|
|
||||||
import io.ebean.docstore.RawDoc;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Document storage operations.
|
|
||||||
*/
|
|
||||||
@NullMarked
|
|
||||||
public interface DocumentStore {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the associated document store using the result of the query.
|
|
||||||
* <p>
|
|
||||||
* This will execute the query against the database creating a document for each
|
|
||||||
* bean graph and sending this to the document store.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Note that the select and fetch paths of the query is set for you to match the
|
|
||||||
* document structure needed based on <code>@DocStore</code> and <code>@DocStoreEmbedded</code>
|
|
||||||
* so what this query requires is the predicates only.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This query will be executed using findEach so it is safe to use a query
|
|
||||||
* that will fetch a lot of beans. The default bulkBatchSize is used.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param query The query that selects object to send to the document store.
|
|
||||||
*/
|
|
||||||
<T> void indexByQuery(Query<T> query);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the associated document store index using the result of the query additionally specifying a
|
|
||||||
* bulkBatchSize to use for sending the messages to ElasticSearch.
|
|
||||||
*
|
|
||||||
* @param query The query that selects object to send to the document store.
|
|
||||||
* @param bulkBatchSize The batch size to use when bulk sending to the document store.
|
|
||||||
*/
|
|
||||||
<T> void indexByQuery(Query<T> query, int bulkBatchSize);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the document store for all beans of this type.
|
|
||||||
* <p>
|
|
||||||
* This is the same as indexByQuery where the query has no predicates and so fetches all rows.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
void indexAll(Class<?> beanType);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the bean by fetching it's content from the document store.
|
|
||||||
* If the document is not found null is returned.
|
|
||||||
* <p>
|
|
||||||
* Typically this is called indirectly by findOne() on the query.
|
|
||||||
* </p>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* Customer customer =
|
|
||||||
* database.find(Customer.class)
|
|
||||||
* .setUseDocStore(true)
|
|
||||||
* .setId(42)
|
|
||||||
* .findOne();
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
<T> T find(DocQueryContext<T> request);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the find list query. This request is prepared to execute secondary queries.
|
|
||||||
* <p>
|
|
||||||
* Typically this is called indirectly by findList() on the query that has setUseDocStore(true).
|
|
||||||
* </p>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* List<Customer> newCustomers =
|
|
||||||
* database.find(Customer.class)
|
|
||||||
* .setUseDocStore(true)
|
|
||||||
* .where().eq("status, Customer.Status.NEW)
|
|
||||||
* .findList();
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*/
|
|
||||||
<T> List<T> findList(DocQueryContext<T> request);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query against the document store returning the paged list.
|
|
||||||
* <p>
|
|
||||||
* The query should have <code>firstRow</code> or <code>maxRows</code> set prior to calling this method.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Typically this is called indirectly by findPagedList() on the query that has setUseDocStore(true).
|
|
||||||
* </p>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* PagedList<Customer> newCustomers =
|
|
||||||
* database.find(Customer.class)
|
|
||||||
* .setUseDocStore(true)
|
|
||||||
* .where().eq("status, Customer.Status.NEW)
|
|
||||||
* .setMaxRows(50)
|
|
||||||
* .findPagedList();
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*/
|
|
||||||
<T> PagedList<T> findPagedList(DocQueryContext<T> request);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query against the document store with the expectation of a large set of results
|
|
||||||
* that are processed in a scrolling resultSet fashion.
|
|
||||||
* <p>
|
|
||||||
* For example, with the ElasticSearch doc store this uses SCROLL.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Typically this is called indirectly by findEach() on the query that has setUseDocStore(true).
|
|
||||||
* </p>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* database.find(Order.class)
|
|
||||||
* .setUseDocStore(true)
|
|
||||||
* .where()... // perhaps add predicates
|
|
||||||
* .findEach((Order order) -> {
|
|
||||||
* // process the bean ...
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*/
|
|
||||||
<T> void findEach(DocQueryContext<T> query, Consumer<T> consumer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query against the document store with the expectation of a large set of results
|
|
||||||
* that are processed in a scrolling resultSet fashion.
|
|
||||||
* <p>
|
|
||||||
* Unlike findEach() this provides the opportunity to stop iterating through the large query.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* For example, with the ElasticSearch doc store this uses SCROLL.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* Typically this is called indirectly by findEachWhile() on the query that has setUseDocStore(true).
|
|
||||||
* </p>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* database.find(Order.class)
|
|
||||||
* .setUseDocStore(true)
|
|
||||||
* .where()... // perhaps add predicates
|
|
||||||
* .findEachWhile(new Predicate<Order>() {
|
|
||||||
* @Override
|
|
||||||
* public void accept(Order bean) {
|
|
||||||
* // process the bean
|
|
||||||
*
|
|
||||||
* // return true to continue, false to stop
|
|
||||||
* // boolean shouldContinue = ...
|
|
||||||
* return shouldContinue;
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*/
|
|
||||||
<T> void findEachWhile(DocQueryContext<T> query, Predicate<T> consumer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find each processing raw documents.
|
|
||||||
*
|
|
||||||
* @param indexNameType The full index name and type
|
|
||||||
* @param rawQuery The query to execute
|
|
||||||
* @param consumer Consumer to process each document
|
|
||||||
*/
|
|
||||||
void findEach(String indexNameType, String rawQuery, Consumer<RawDoc> consumer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find each processing raw documents stopping when the predicate returns false.
|
|
||||||
*
|
|
||||||
* @param indexNameType The full index name and type
|
|
||||||
* @param rawQuery The query to execute
|
|
||||||
* @param consumer Consumer to process each document until false is returned
|
|
||||||
*/
|
|
||||||
void findEachWhile(String indexNameType, String rawQuery, Predicate<RawDoc> consumer);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process the queue entries sending updates to the document store or queuing them for later processing.
|
|
||||||
*/
|
|
||||||
long process(List<DocStoreQueueEntry> queueEntries) throws IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drop the index from the document store (similar to DDL drop table).
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* DocumentStore documentStore = database.docStore();
|
|
||||||
*
|
|
||||||
* documentStore.dropIndex("product_copy");
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*/
|
|
||||||
void dropIndex(String indexName);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an index given a mapping file as a resource in the classPath (similar to DDL create table).
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* DocumentStore documentStore = database.docStore();
|
|
||||||
*
|
|
||||||
* // uses product_copy.mapping.json resource
|
|
||||||
* // ... to define mappings for the index
|
|
||||||
*
|
|
||||||
* documentStore.createIndex("product_copy", null);
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* @param indexName the name of the new index
|
|
||||||
* @param alias the alias of the index
|
|
||||||
*/
|
|
||||||
void createIndex(String indexName, String alias);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modify the settings on an index.
|
|
||||||
* <p>
|
|
||||||
* For example, this can be used be used to set elasticSearch refresh_interval
|
|
||||||
* on an index before a bulk update.
|
|
||||||
* </p>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* // refresh_interval -1 ... disable refresh while bulk loading
|
|
||||||
*
|
|
||||||
* Map<String,Object> settings = new LinkedHashMap<>();
|
|
||||||
* settings.put("refresh_interval", "-1");
|
|
||||||
*
|
|
||||||
* documentStore.indexSettings("product", settings);
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* // refresh_interval 1s ... restore after bulk loading
|
|
||||||
*
|
|
||||||
* Map<String,Object> settings = new LinkedHashMap<>();
|
|
||||||
* settings.put("refresh_interval", "1s");
|
|
||||||
*
|
|
||||||
* documentStore.indexSettings("product", settings);
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* @param indexName the name of the index to update settings on
|
|
||||||
* @param settings the settings to set on the index
|
|
||||||
*/
|
|
||||||
void indexSettings(String indexName, Map<String, Object> settings);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy the index to a new index.
|
|
||||||
* <p>
|
|
||||||
* This copy process does not use the database but instead will copy from the source index to a destination index.
|
|
||||||
* </p>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* long copyCount = documentStore.copyIndex(Product.class, "product_copy");
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* @param beanType The bean type of the source index
|
|
||||||
* @param newIndex The name of the index to copy to
|
|
||||||
* @return the number of documents copied to the new index
|
|
||||||
*/
|
|
||||||
long copyIndex(Class<?> beanType, String newIndex);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy entries from an index to a new index but limiting to documents that have been
|
|
||||||
* modified since the sinceEpochMillis time.
|
|
||||||
* <p>
|
|
||||||
* To support this the document needs to have a <code>@WhenModified</code> property.
|
|
||||||
* </p>
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* long copyCount = documentStore.copyIndex(Product.class, "product_copy", sinceMillis);
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* @param beanType The bean type of the source index
|
|
||||||
* @param newIndex The name of the index to copy to
|
|
||||||
* @return the number of documents copied to the new index
|
|
||||||
*/
|
|
||||||
long copyIndex(Class<?> beanType, String newIndex, long sinceEpochMillis);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy from a source index to a new index taking only the documents
|
|
||||||
* matching the given query.
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* // predicates to select the source documents to copy
|
|
||||||
* Query<Product> query = database.find(Product.class)
|
|
||||||
* .where()
|
|
||||||
* .ge("whenModified", new Timestamp(since))
|
|
||||||
* .ge("name", "A")
|
|
||||||
* .lt("name", "D")
|
|
||||||
* .query();
|
|
||||||
*
|
|
||||||
* // copy from the source index to "product_copy" index
|
|
||||||
* long copyCount = documentStore.copyIndex(query, "product_copy", 1000);
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* @param query The query to select the source documents to copy
|
|
||||||
* @param newIndex The target index to copy the documents to
|
|
||||||
* @param bulkBatchSize The ElasticSearch bulk batch size, if 0 uses the default.
|
|
||||||
* @return The number of documents copied to the new index.
|
|
||||||
*/
|
|
||||||
long copyIndex(Query<?> query, String newIndex, int bulkBatchSize);
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package io.ebean;
|
|
||||||
|
|
||||||
import jakarta.persistence.PersistenceException;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static bridge registering custom {@code @DtoConvert} converter instances so generated DTO
|
|
||||||
* mappers can reach them.
|
|
||||||
* <p>
|
|
||||||
* Generated mappers (see {@code query.mapTo(SomeDto.class)}) are wired via {@code ServiceLoader}
|
|
||||||
* as plain, no-arg-constructed, compile-time singletons (mirroring how entity/query-bean
|
|
||||||
* registration already works) - they have no way to reach a dependency-injection container, or
|
|
||||||
* any particular {@code Database} instance, at construction time. When a
|
|
||||||
* {@code @DtoConvert(value = ConverterType.class, method = "...")} property's converter is an
|
|
||||||
* <b>instance</b> method (as opposed to a {@code static} one, which is called directly with no
|
|
||||||
* registration needed at all), the generated mapper resolves it via {@link #get(Class)} - so the
|
|
||||||
* application must register an instance here, typically one already built by its own DI
|
|
||||||
* container, <b>before</b> building the {@code Database}:
|
|
||||||
* <pre>{@code
|
|
||||||
* AES256Cipher cipher = ...; // already DI-constructed
|
|
||||||
* DtoConverterManager.put(DriverConversions.class, new DriverConversionsImpl(cipher));
|
|
||||||
*
|
|
||||||
* Database db = DatabaseFactory.create(...); // generated mappers resolve converters from here
|
|
||||||
* }</pre>
|
|
||||||
* <p>
|
|
||||||
* This is a deliberate, narrowly-scoped exception to preferring dependency injection over static
|
|
||||||
* mutable state - it exists solely to bridge an already-DI-constructed singleton into
|
|
||||||
* {@code ServiceLoader}-discovered, no-arg-constructed generated code, which cannot otherwise
|
|
||||||
* reach a DI container or a specific {@code Database} instance. {@link #get(Class)} throws
|
|
||||||
* immediately if nothing was registered for the given type, so a missing/late registration fails
|
|
||||||
* fast at {@code Database} build time (a generated mapper's eager field initializer) rather than
|
|
||||||
* lazily on first use.
|
|
||||||
*/
|
|
||||||
public final class DtoConverterManager {
|
|
||||||
|
|
||||||
private static final Map<Class<?>, Object> converters = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private DtoConverterManager() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a converter instance for the given type - must be called before the
|
|
||||||
* {@code Database} using it is built.
|
|
||||||
*/
|
|
||||||
public static <T> void put(Class<T> type, T instance) {
|
|
||||||
converters.put(type, instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the registered converter instance for the given type.
|
|
||||||
*
|
|
||||||
* @throws PersistenceException if no instance was registered for {@code type}.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T> T get(Class<T> type) {
|
|
||||||
T instance = (T) converters.get(type);
|
|
||||||
if (instance == null) {
|
|
||||||
throw new PersistenceException("No " + type.getName() + " registered - call "
|
|
||||||
+ "DtoConverterManager.put(" + type.getSimpleName() + ".class, ...) before starting the Database");
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package io.ebean;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.IdentityHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identity-keyed cache of already-mapped source -> target instances, shared across one
|
|
||||||
* top-level {@link DtoMapper#mapList(java.util.List)} call (or an explicitly shared context).
|
|
||||||
* <p>
|
|
||||||
* Keyed by source object <b>identity</b> (an {@link IdentityHashMap}, not {@code equals()}/
|
|
||||||
* {@code hashCode()}) because the source is an Ebean entity graph, where repeated references to
|
|
||||||
* the same row within one query already resolve to the same Java object instance.
|
|
||||||
* <p>
|
|
||||||
* The identity map is partitioned <b>per target DTO type</b>. This matters because the same
|
|
||||||
* source instance can legitimately need to be mapped to more than one target type within a
|
|
||||||
* single graph - e.g. a top-level {@code CustomerDtoMapper} maps a {@code Customer} to a full
|
|
||||||
* {@code CustomerDto}, while a nested {@code ContactDtoMapper} maps the very same {@code Customer}
|
|
||||||
* instance (accessed via {@code contact.getCustomer()}) to a shallow {@code CustomerRefDto} to
|
|
||||||
* avoid a cycle. A single un-partitioned {@code IdentityHashMap<Object,Object>} would have the
|
|
||||||
* two mappers collide on the same source key and incorrectly hand back the other mapper's
|
|
||||||
* (wrong-typed) cached result. Partitioning by target type keeps each mapper's cache isolated
|
|
||||||
* while still sharing one context/instance per top-level mapping call.
|
|
||||||
* <p>
|
|
||||||
* Not thread-safe - a context is expected to be created per top-level mapping call and not
|
|
||||||
* shared across threads.
|
|
||||||
*/
|
|
||||||
public final class DtoMapContext {
|
|
||||||
|
|
||||||
private final Map<Class<?>, Map<Object, Object>> mappedByType = new HashMap<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the already-mapped target for the given source instance if present, otherwise map it
|
|
||||||
* via {@code mappingFunction}, register it, and return it.
|
|
||||||
*
|
|
||||||
* @param targetType the DTO type being produced - used to partition the identity cache so that
|
|
||||||
* mapping the same source to different target types never collides.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public <S, T> T computeIfAbsent(Class<T> targetType, S source, Function<S, T> mappingFunction) {
|
|
||||||
Map<Object, Object> mapped = mappedByType.computeIfAbsent(targetType, t -> new IdentityHashMap<>());
|
|
||||||
T existing = (T) mapped.get(source);
|
|
||||||
if (existing != null) {
|
|
||||||
return existing;
|
|
||||||
}
|
|
||||||
T created = mappingFunction.apply(source);
|
|
||||||
mapped.put(source, created);
|
|
||||||
return created;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package io.ebean;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapper interface implemented by generated (or hand-written) entity -> DTO graph mappers.
|
|
||||||
* <p>
|
|
||||||
* Used with nested entity-to-DTO graph mapping (see {@code query.mapTo(SomeDto.class)}) as
|
|
||||||
* distinct from the existing flat, single-row {@link DtoQuery} pipeline. Each entity/DTO type
|
|
||||||
* pair gets its own small, composable mapper implementation (mirroring MapStruct's per-type
|
|
||||||
* mapper generation) rather than one large mapper inlining every nested type. Nested mappers are
|
|
||||||
* wired together via constructor injection, not static singletons - this keeps mappers stateless,
|
|
||||||
* substitutable (e.g. for tests) and avoids global mutable state.
|
|
||||||
* <p>
|
|
||||||
* A {@link DtoMapContext} is threaded through every nested {@code map(...)} call within one
|
|
||||||
* top-level {@link #mapList(List)} invocation, so that repeated references to the same source
|
|
||||||
* entity instance (e.g. several {@code Contact}s sharing the same {@code Customer}) map to the
|
|
||||||
* <b>same</b> target DTO instance rather than creating duplicate-but-equal copies. This mirrors
|
|
||||||
* the identity semantics Ebean's own entity graph already has, and is what makes the resulting
|
|
||||||
* DTO graph "graph shaped" rather than "tree of copies shaped".
|
|
||||||
* <p>
|
|
||||||
* Implementations contain no reflection or {@code MethodHandles} - only direct getter calls and
|
|
||||||
* constructor invocation - so generated mappers are safe under GraalVM native-image with zero
|
|
||||||
* additional reachability metadata.
|
|
||||||
*
|
|
||||||
* @param <SOURCE> the source entity (or embeddable) type
|
|
||||||
* @param <TARGET> the target DTO type
|
|
||||||
*/
|
|
||||||
public interface DtoMapper<SOURCE, TARGET> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the {@link FetchGroup} of exactly the source properties (and nested paths) needed to
|
|
||||||
* populate the target DTO graph - the select()/fetch() spec is derived from the DTO's declared
|
|
||||||
* shape rather than maintained separately by hand. Used by {@code query.mapTo(TARGET.class)}
|
|
||||||
* to automatically apply the correct fetch spec before the query is executed.
|
|
||||||
*/
|
|
||||||
FetchGroup<SOURCE> fetchGroup();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a single source instance to its target DTO, reusing/registering the mapping in the
|
|
||||||
* given context so that repeated references to the same source instance de-duplicate to the
|
|
||||||
* same target instance. Must return {@code null} when given {@code null}.
|
|
||||||
*/
|
|
||||||
TARGET map(SOURCE source, DtoMapContext context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a single source instance using a fresh, one-off context. Convenience for mapping a
|
|
||||||
* single object in isolation (no de-duplication opportunity since there's nothing else in
|
|
||||||
* scope to de-duplicate against).
|
|
||||||
*/
|
|
||||||
default TARGET map(SOURCE source) {
|
|
||||||
return map(source, new DtoMapContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a list of source instances to a list of target DTOs sharing the given context,
|
|
||||||
* preserving order.
|
|
||||||
*/
|
|
||||||
default List<TARGET> mapList(List<SOURCE> source, DtoMapContext context) {
|
|
||||||
List<TARGET> result = new ArrayList<>(source.size());
|
|
||||||
for (SOURCE s : source) {
|
|
||||||
result.add(map(s, context));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map a list of source instances to a list of target DTOs using a fresh context shared across
|
|
||||||
* the whole list - this is the usual top-level entry point, e.g. mapping the result of a
|
|
||||||
* {@code query.findList()} call.
|
|
||||||
*/
|
|
||||||
default List<TARGET> mapList(List<SOURCE> source) {
|
|
||||||
return mapList(source, new DtoMapContext());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package io.ebean;
|
|
||||||
|
|
||||||
import io.ebean.config.DtoMapperRegister;
|
|
||||||
import jakarta.persistence.PersistenceException;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.ServiceLoader;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads all generated {@link DtoMapperRegister} implementations (via {@code ServiceLoader},
|
|
||||||
* mirroring how {@code EntityClassRegister} is discovered) once, and resolves the {@link
|
|
||||||
* DtoMapper} for a given (source, dto) pair, or by the generated mapper's own concrete type, on
|
|
||||||
* request.
|
|
||||||
* <p>
|
|
||||||
* Has no dependency on {@link Database} - it can be constructed independently, before (or
|
|
||||||
* without) a {@code Database} existing at all, e.g. as a DI-managed singleton constructed
|
|
||||||
* alongside the rest of an application's dependency graph. If you want the exact same instance
|
|
||||||
* (and hence the exact same underlying mapper instances) shared between {@code query.mapTo(...)}
|
|
||||||
* and your own application code, construct it yourself and register it via {@code
|
|
||||||
* DatabaseBuilder.putServiceObject(DtoMapperManager.class, myManager)} before building the {@code
|
|
||||||
* Database} - it is then used instead of a Database-internal default instance.
|
|
||||||
* <p>
|
|
||||||
* Resolved mappers are cached so that repeated lookups only ever pay the cost of iterating the
|
|
||||||
* generated registers and constructing the mapper (and its nested mapper/{@code FetchGroup}
|
|
||||||
* graph) once - after that, every lookup is a single hash-map hit regardless of how many entity/
|
|
||||||
* DTO pairs are registered.
|
|
||||||
*/
|
|
||||||
public final class DtoMapperManager {
|
|
||||||
|
|
||||||
private final List<DtoMapperRegister> registers;
|
|
||||||
private final ConcurrentHashMap<MapperKey, DtoMapper<?, ?>> pairCache = new ConcurrentHashMap<>();
|
|
||||||
private final ConcurrentHashMap<Class<?>, Object> typeCache = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public DtoMapperManager() {
|
|
||||||
this.registers = load();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<DtoMapperRegister> load() {
|
|
||||||
List<DtoMapperRegister> result = new ArrayList<>();
|
|
||||||
for (DtoMapperRegister register : ServiceLoader.load(DtoMapperRegister.class)) {
|
|
||||||
result.add(register);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the {@link DtoMapper} for the given (source, dto) pair.
|
|
||||||
*
|
|
||||||
* @throws PersistenceException if no generated mapper is registered for that pair.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public <S, D> DtoMapper<S, D> mapperFor(Class<S> sourceType, Class<D> dtoType) {
|
|
||||||
return (DtoMapper<S, D>) pairCache.computeIfAbsent(new MapperKey(sourceType, dtoType), this::resolve);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the generated mapper instance of the given concrete mapper type - e.g. {@code
|
|
||||||
* manager.get(CustomerDtoMapper.class)} - typically used to resolve a mapper instance for
|
|
||||||
* dependency injection into application code (e.g. an avaje-inject {@code @Factory} bean
|
|
||||||
* method).
|
|
||||||
*
|
|
||||||
* @throws PersistenceException if no generated mapper of that type is registered.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public <T> T get(Class<T> mapperType) {
|
|
||||||
return (T) typeCache.computeIfAbsent(mapperType, this::resolveByType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DtoMapper<?, ?> resolve(MapperKey key) {
|
|
||||||
for (DtoMapperRegister register : registers) {
|
|
||||||
DtoMapper<?, ?> mapper = register.mapperFor(key.sourceType, key.dtoType);
|
|
||||||
if (mapper != null) {
|
|
||||||
return mapper;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new PersistenceException("No DtoMapper registered mapping " + key.sourceType + " -> " + key.dtoType
|
|
||||||
+ " - check @DtoMapping(source = " + key.sourceType.getSimpleName() + ".class, target = "
|
|
||||||
+ key.dtoType.getSimpleName() + ".class) is declared on a package-info.java processed by querybean-generator");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object resolveByType(Class<?> mapperType) {
|
|
||||||
for (DtoMapperRegister register : registers) {
|
|
||||||
Object mapper = register.mapperOfType(mapperType);
|
|
||||||
if (mapper != null) {
|
|
||||||
return mapper;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new PersistenceException("No DtoMapper of type " + mapperType.getName() + " registered"
|
|
||||||
+ " - check a @DtoMapping(...) pair generating " + mapperType.getSimpleName()
|
|
||||||
+ " is declared on a package-info.java processed by querybean-generator");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache key pairing the source entity type and target DTO type.
|
|
||||||
*/
|
|
||||||
private static final class MapperKey {
|
|
||||||
|
|
||||||
private final Class<?> sourceType;
|
|
||||||
private final Class<?> dtoType;
|
|
||||||
|
|
||||||
MapperKey(Class<?> sourceType, Class<?> dtoType) {
|
|
||||||
this.sourceType = sourceType;
|
|
||||||
this.dtoType = dtoType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!(o instanceof MapperKey)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
MapperKey other = (MapperKey) o;
|
|
||||||
return sourceType == other.sourceType && dtoType == other.dtoType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return 31 * sourceType.hashCode() + dtoType.hashCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
package io.ebean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime helpers used by generated {@link DtoMapper} implementations to safely resolve a
|
|
||||||
* primitive-typed DTO field whose value is derived from a multi-hop {@code @DtoPath} that
|
|
||||||
* traverses a nullable intermediate relation.
|
|
||||||
* <p>
|
|
||||||
* A {@code null}-guarded getter-chain (e.g. {@code source.getOrganisation() == null ? null :
|
|
||||||
* source.getOrganisation().getId()}) always types as the boxed wrapper (since one branch is the
|
|
||||||
* {@code null} literal). When the DTO's target field is a primitive (e.g. {@code long
|
|
||||||
* organisationId}), passing that boxed expression to the constructor auto-unboxes it - which
|
|
||||||
* throws a raw, unhelpful {@link NullPointerException} if the relation really is {@code null}.
|
|
||||||
* <p>
|
|
||||||
* These methods give the generated mapper a choice, controlled by {@code @DtoPath#failOnNull()}:
|
|
||||||
* default to the primitive's zero-equivalent value ({@code orZero} methods, the default), or
|
|
||||||
* throw a clear, descriptive exception naming the offending property path ({@code require}
|
|
||||||
* methods, opted into via {@code failOnNull = true}).
|
|
||||||
*
|
|
||||||
* @see io.ebean.annotation.DtoPath
|
|
||||||
*/
|
|
||||||
public final class DtoMapperSupport {
|
|
||||||
|
|
||||||
private DtoMapperSupport() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
|
||||||
public static long orZero(Long value) {
|
|
||||||
return value == null ? 0L : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
|
||||||
public static int orZero(Integer value) {
|
|
||||||
return value == null ? 0 : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
|
||||||
public static short orZero(Short value) {
|
|
||||||
return value == null ? 0 : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return {@code 0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
|
||||||
public static byte orZero(Byte value) {
|
|
||||||
return value == null ? 0 : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return {@code 0.0} if {@code value} is {@code null}, otherwise its unboxed value. */
|
|
||||||
public static double orZero(Double value) {
|
|
||||||
return value == null ? 0.0 : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return {@code 0.0f} if {@code value} is {@code null}, otherwise its unboxed value. */
|
|
||||||
public static float orZero(Float value) {
|
|
||||||
return value == null ? 0.0f : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return {@code false} if {@code value} is {@code null}, otherwise its unboxed value. */
|
|
||||||
public static boolean orZero(Boolean value) {
|
|
||||||
return value != null && value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return {@code '\u0000'} if {@code value} is {@code null}, otherwise its unboxed value. */
|
|
||||||
public static char orZero(Character value) {
|
|
||||||
return value == null ? '\u0000' : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
|
||||||
public static long require(Long value, String path) {
|
|
||||||
if (value == null) {
|
|
||||||
throw failure(path);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
|
||||||
public static int require(Integer value, String path) {
|
|
||||||
if (value == null) {
|
|
||||||
throw failure(path);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
|
||||||
public static short require(Short value, String path) {
|
|
||||||
if (value == null) {
|
|
||||||
throw failure(path);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
|
||||||
public static byte require(Byte value, String path) {
|
|
||||||
if (value == null) {
|
|
||||||
throw failure(path);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
|
||||||
public static double require(Double value, String path) {
|
|
||||||
if (value == null) {
|
|
||||||
throw failure(path);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
|
||||||
public static float require(Float value, String path) {
|
|
||||||
if (value == null) {
|
|
||||||
throw failure(path);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
|
||||||
public static boolean require(Boolean value, String path) {
|
|
||||||
if (value == null) {
|
|
||||||
throw failure(path);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return the unboxed value, or throw if {@code value} is {@code null}. */
|
|
||||||
public static char require(Character value, String path) {
|
|
||||||
if (value == null) {
|
|
||||||
throw failure(path);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IllegalStateException failure(String path) {
|
|
||||||
return new IllegalStateException(
|
|
||||||
"@DtoPath(\"" + path + "\") resolved to null via a nullable intermediate relation, but the"
|
|
||||||
+ " target DTO field is primitive and failOnNull=true - either handle the null case in"
|
|
||||||
+ " source data, use a boxed wrapper type for the DTO field, or remove failOnNull to"
|
|
||||||
+ " default to the primitive's zero-equivalent value instead.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
|
import io.avaje.lang.Nullable;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query for performing native SQL queries that return DTO Bean's.
|
* Query for performing native SQL queries that return DTO Bean's.
|
||||||
@@ -35,8 +40,13 @@ import java.util.List;
|
|||||||
*
|
*
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
public interface DtoQuery<T> extends CancelableQuery {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the query returning a list.
|
||||||
|
*/
|
||||||
|
List<T> findList();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the query iterating a row at a time.
|
* Execute the query iterating a row at a time.
|
||||||
@@ -47,6 +57,56 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
|||||||
*/
|
*/
|
||||||
QueryIterator<T> findIterate();
|
QueryIterator<T> findIterate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the query returning a Stream.
|
||||||
|
* <p>
|
||||||
|
* Note that the Stream holds resources related to the underlying
|
||||||
|
* resultSet and potentially connection and MUST be closed. We should use
|
||||||
|
* the Stream in a <em>try with resource block</em>.
|
||||||
|
*/
|
||||||
|
Stream<T> findStream();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the query iterating a row at a time.
|
||||||
|
* <p>
|
||||||
|
* This streaming type query is useful for large query execution as only 1 row needs to be held in memory.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
void findEach(Consumer<T> consumer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the query iterating the results and batching them for the consumer.
|
||||||
|
* <p>
|
||||||
|
* This runs like findEach streaming results from the database but just collects the results
|
||||||
|
* into batches to pass to the consumer.
|
||||||
|
*
|
||||||
|
* @param batch The number of dto beans to collect before given them to the consumer
|
||||||
|
* @param consumer The consumer to process the batch of DTO beans
|
||||||
|
*/
|
||||||
|
void findEach(int batch, Consumer<List<T>> consumer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the query iterating a row at a time with the ability to stop consuming part way through.
|
||||||
|
* <p>
|
||||||
|
* Returning false after processing a row stops the iteration through the query results.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* This streaming type query is useful for large query execution as only 1 row needs to be held in memory.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
void findEachWhile(Predicate<T> consumer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the query returning a single bean.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
T findOne();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the query returning an optional bean.
|
||||||
|
*/
|
||||||
|
Optional<T> findOneOrEmpty();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind all the parameters using index positions.
|
* Bind all the parameters using index positions.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -78,17 +138,6 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
|||||||
* Bind the named multi-value array parameter which we would use with Postgres ANY.
|
* Bind the named multi-value array parameter which we would use with Postgres ANY.
|
||||||
* <p>
|
* <p>
|
||||||
* For Postgres this binds an ARRAY rather than expands into multiple bind values.
|
* 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);
|
DtoQuery<T> setArrayParameter(String name, Collection<?> values);
|
||||||
|
|
||||||
@@ -153,41 +202,19 @@ public interface DtoQuery<T> extends StreamableQuery<DtoQuery<T>, T> {
|
|||||||
DtoQuery<T> setBufferFetchSizeHint(int bufferFetchSizeHint);
|
DtoQuery<T> setBufferFetchSizeHint(int bufferFetchSizeHint);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a PagedList for this query using firstRow and maxRows.
|
* Use the explicit transaction to execute the query.
|
||||||
* <p>
|
|
||||||
* The benefit of using this over findList() is that it provides functionality to get the
|
|
||||||
* total row count etc.
|
|
||||||
* <p>
|
|
||||||
* If maxRows is not set on the query prior to calling findPagedList() then a
|
|
||||||
* PersistenceException is thrown.
|
|
||||||
* <p>
|
|
||||||
* This is only supported for a DtoQuery that is derived from an ORM query via
|
|
||||||
* {@link Query#asDto(Class)} / {@link ExpressionList#asDto(Class)}. It is not supported
|
|
||||||
* for a DtoQuery based on raw SQL (e.g. via {@link Database#findDto(Class, String)}) as
|
|
||||||
* there is no query structure available from which to derive a matching row count query -
|
|
||||||
* a PersistenceException is thrown in that case.
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* PagedList<OrderDto> pagedList =
|
|
||||||
* DB.find(Order.class)
|
|
||||||
* .where().eq("status", Order.Status.NEW)
|
|
||||||
* .orderBy().asc("id")
|
|
||||||
* .setFirstRow(50)
|
|
||||||
* .setMaxRows(20)
|
|
||||||
* .asDto(OrderDto.class)
|
|
||||||
* .findPagedList();
|
|
||||||
*
|
|
||||||
* // fetch the total row count in the background
|
|
||||||
* pagedList.loadCount();
|
|
||||||
*
|
|
||||||
* List<OrderDto> orders = pagedList.getList();
|
|
||||||
* int totalRowCount = pagedList.getTotalCount();
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* @return The PagedList
|
|
||||||
*/
|
*/
|
||||||
@Override
|
DtoQuery<T> usingTransaction(Transaction transaction);
|
||||||
PagedList<T> findPagedList();
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that the master DataSource is used if there is a read only data source
|
||||||
|
* being used (that is using a read replica database potentially with replication lag).
|
||||||
|
* <p>
|
||||||
|
* When the database is configured with a read-only DataSource via
|
||||||
|
* say {@link io.ebean.config.DatabaseConfig#setReadOnlyDataSource(DataSource)} then
|
||||||
|
* by default when a query is run without an active transaction, it uses the read-only data
|
||||||
|
* source. We we use {@code usingMaster()} to instead ensure that the query is executed
|
||||||
|
* against the master data source.
|
||||||
|
*/
|
||||||
|
DtoQuery<T> usingMaster();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import java.util.List;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated migrate to using {@link PagedList#emptyList()} only.
|
|
||||||
* <p>
|
|
||||||
* An empty PagedList.
|
* An empty PagedList.
|
||||||
* <p>
|
* <p>
|
||||||
* For use in application code when we need to return a PagedList but don't want to
|
* 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>
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
@Deprecated(forRemoval = true)
|
public class EmptyPagedList<T> implements PagedList<T> {
|
||||||
public final class EmptyPagedList<T> implements PagedList<T> {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadCount() {
|
public void loadCount() {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import io.ebean.search.*;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -625,31 +623,6 @@ public interface ExpressionFactory {
|
|||||||
*/
|
*/
|
||||||
Expression raw(String raw);
|
Expression raw(String raw);
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a Text Match expression (currently doc store/Elastic only).
|
|
||||||
*/
|
|
||||||
Expression textMatch(String propertyName, String search, Match options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a Text Multi match expression (currently doc store/Elastic only).
|
|
||||||
*/
|
|
||||||
Expression textMultiMatch(String query, MultiMatch options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a text simple query expression (currently doc store/Elastic only).
|
|
||||||
*/
|
|
||||||
Expression textSimple(String search, TextSimple options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a text query string expression (currently doc store/Elastic only).
|
|
||||||
*/
|
|
||||||
Expression textQueryString(String search, TextQueryString options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a text common terms expression (currently doc store/Elastic only).
|
|
||||||
*/
|
|
||||||
Expression textCommonTerms(String search, TextCommonTerms options);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* And - join two expressions with a logical and.
|
* And - join two expressions with a logical and.
|
||||||
*/
|
*/
|
||||||
@@ -693,12 +666,4 @@ public interface ExpressionFactory {
|
|||||||
*/
|
*/
|
||||||
<T> Junction<T> junction(Junction.Type type, Query<T> query, ExpressionList<T> parent);
|
<T> Junction<T> junction(Junction.Type type, Query<T> query, ExpressionList<T> parent);
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the expressions to the given expression list.
|
|
||||||
*
|
|
||||||
* @param where The expression list to add the expressions to
|
|
||||||
* @param expressions The expressions that are parsed
|
|
||||||
* @param params Bind parameters to match ? or ?1 bind positions.
|
|
||||||
*/
|
|
||||||
<T> void where(ExpressionList<T> where, String expressions, Object[] params);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
import org.jspecify.annotations.Nullable;
|
import io.avaje.lang.Nullable;
|
||||||
import io.ebean.search.*;
|
|
||||||
|
|
||||||
import jakarta.persistence.NonUniqueResultException;
|
import jakarta.persistence.NonUniqueResultException;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
@@ -10,7 +9,6 @@ import java.sql.Timestamp;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of Expressions that make up a where or having clause.
|
* List of Expressions that make up a where or having clause.
|
||||||
@@ -32,7 +30,7 @@ import java.util.function.Supplier;
|
|||||||
*
|
*
|
||||||
* @see Query#where()
|
* @see Query#where()
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public interface ExpressionList<T> {
|
public interface ExpressionList<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,6 +52,14 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
Query<T> orderById(boolean orderById);
|
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
|
* Set the order by clause replacing the existing order by clause if there is
|
||||||
* one.
|
* one.
|
||||||
@@ -64,6 +70,14 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
ExpressionList<T> orderBy(String orderBy);
|
ExpressionList<T> orderBy(String orderBy);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deprecated migrate to orderBy().
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
|
default OrderBy<T> order() {
|
||||||
|
return orderBy();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the OrderBy so that you can append an ascending or descending
|
* Return the OrderBy so that you can append an ascending or descending
|
||||||
* property to the order by clause.
|
* property to the order by clause.
|
||||||
@@ -91,11 +105,6 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
Query<T> asOf(Timestamp asOf);
|
Query<T> asOf(Timestamp asOf);
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query against the draft set of tables.
|
|
||||||
*/
|
|
||||||
Query<T> asDraft();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the query to a DTO bean query.
|
* Convert the query to a DTO bean query.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -104,29 +113,6 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
<D> DtoQuery<D> asDto(Class<D> dtoClass);
|
<D> DtoQuery<D> asDto(Class<D> dtoClass);
|
||||||
|
|
||||||
/**
|
|
||||||
* Map the query result to a nested DTO graph, automatically deriving the select()/fetch() spec
|
|
||||||
* from the target DTO's declared shape and forcing {@code setUnmodifiable(true)}.
|
|
||||||
* <p>
|
|
||||||
* Distinct from {@link #asDto(Class)} (the flat, single-row SQL pipeline) - this supports
|
|
||||||
* nested ToOne/ToMany DTO graphs, mapped from the normal ORM entity query result.
|
|
||||||
*
|
|
||||||
* @throws jakarta.persistence.PersistenceException if no generated {@link DtoMapper} is
|
|
||||||
* registered for this (entity, dto) pair.
|
|
||||||
*/
|
|
||||||
<D> MappedQuery<D> mapTo(Class<D> dtoType);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map the query result to a nested DTO graph using an already-resolved {@link DtoMapper}
|
|
||||||
* instance, rather than looking one up by (entity, dtoType) - e.g. to select a named variant
|
|
||||||
* mapper (see {@code @DtoMapping(name = "...", exclude = "...")}), such as
|
|
||||||
* {@code query.mapTo(User.class, userMapper.noFleets())}.
|
|
||||||
*
|
|
||||||
* @param dtoType the DTO type mapped to (must match {@code mapper}'s target type)
|
|
||||||
* @param mapper the mapper instance to use, e.g. a named variant accessor on a generated mapper
|
|
||||||
*/
|
|
||||||
<D> MappedQuery<D> mapTo(Class<D> dtoType, DtoMapper<T, D> mapper);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the underlying query as an UpdateQuery.
|
* Return the underlying query as an UpdateQuery.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -230,20 +216,16 @@ public interface ExpressionList<T> {
|
|||||||
int delete();
|
int delete();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute as a delete query permanently deleting the 'root level' beans that match the
|
* Execute as a delete query deleting the 'root level' beans that match the predicates
|
||||||
* predicates in the query without soft delete.
|
* in the query.
|
||||||
* <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>
|
* <p>
|
||||||
* Note that if the query includes joins then the generated delete statement may not be
|
* Note that if the query includes joins then the generated delete statement may not be
|
||||||
* optimal depending on the database platform.
|
* optimal depending on the database platform.
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @return the number of rows that were permanently deleted.
|
* @return the number of rows that were deleted.
|
||||||
*/
|
*/
|
||||||
int deletePermanent();
|
int delete(Transaction transaction);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute as a update query.
|
* Execute as a update query.
|
||||||
@@ -253,6 +235,14 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
int update();
|
int update();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute as a update query with the given transaction.
|
||||||
|
*
|
||||||
|
* @return the number of rows that were updated.
|
||||||
|
* @see UpdateQuery
|
||||||
|
*/
|
||||||
|
int update(Transaction transaction);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the query returning true if a row is found.
|
* Execute the query returning true if a row is found.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -353,7 +343,7 @@ public interface ExpressionList<T> {
|
|||||||
* List<String> names =
|
* List<String> names =
|
||||||
* DB.find(Customer.class)
|
* DB.find(Customer.class)
|
||||||
* .select("name")
|
* .select("name")
|
||||||
* .orderBy().asc("name")
|
* .order().asc("name")
|
||||||
* .findSingleAttributeList();
|
* .findSingleAttributeList();
|
||||||
*
|
*
|
||||||
* }</pre>
|
* }</pre>
|
||||||
@@ -366,7 +356,7 @@ public interface ExpressionList<T> {
|
|||||||
* .setDistinct(true)
|
* .setDistinct(true)
|
||||||
* .select("name")
|
* .select("name")
|
||||||
* .where().eq("status", Customer.Status.NEW)
|
* .where().eq("status", Customer.Status.NEW)
|
||||||
* .orderBy().asc("name")
|
* .order().asc("name")
|
||||||
* .setMaxRows(100)
|
* .setMaxRows(100)
|
||||||
* .findSingleAttributeList();
|
* .findSingleAttributeList();
|
||||||
*
|
*
|
||||||
@@ -422,62 +412,6 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
Optional<T> findOneOrEmpty();
|
Optional<T> findOneOrEmpty();
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query returning a single bean or throwing a {@link jakarta.persistence.EntityNotFoundException}
|
|
||||||
* if there is no matching bean.
|
|
||||||
*
|
|
||||||
* @see Query#findOneOrThrow()
|
|
||||||
*/
|
|
||||||
default T findOneOrThrow() {
|
|
||||||
return query().findOneOrThrow();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query returning a single bean or throwing the exception produced by the
|
|
||||||
* given supplier if there is no matching bean.
|
|
||||||
*
|
|
||||||
* @see Query#findOneOrThrow(Supplier)
|
|
||||||
*/
|
|
||||||
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
|
||||||
return query().findOneOrThrow(exceptionSupplier);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute find row count query in a background thread.
|
|
||||||
* <p>
|
|
||||||
* This returns a Future object which can be used to cancel, check the
|
|
||||||
* execution status (isDone etc) and get the value (with or without a
|
|
||||||
* timeout).
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return a Future object for the row count query
|
|
||||||
*/
|
|
||||||
FutureRowCount<T> findFutureCount();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute find Id's query in a background thread.
|
|
||||||
* <p>
|
|
||||||
* This returns a Future object which can be used to cancel, check the
|
|
||||||
* execution status (isDone etc) and get the value (with or without a
|
|
||||||
* timeout).
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return a Future object for the list of Id's
|
|
||||||
*/
|
|
||||||
FutureIds<T> findFutureIds();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute find list query in a background thread.
|
|
||||||
* <p>
|
|
||||||
* This returns a Future object which can be used to cancel, check the
|
|
||||||
* execution status (isDone etc) and get the value (with or without a
|
|
||||||
* timeout).
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @return a Future object for the list result of the query
|
|
||||||
*/
|
|
||||||
FutureList<T> findFutureList();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a PagedList for this query using firstRow and maxRows.
|
* Return a PagedList for this query using firstRow and maxRows.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -531,28 +465,6 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
ExpressionList<T> filterMany(String manyProperty);
|
ExpressionList<T> filterMany(String manyProperty);
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated for removal - migrate to {@link #filterManyRaw(String, String, Object...)}.
|
|
||||||
* <p>
|
|
||||||
* Add filter expressions to the many property.
|
|
||||||
*
|
|
||||||
* <pre>{@code
|
|
||||||
*
|
|
||||||
* DB.find(Customer.class)
|
|
||||||
* .where()
|
|
||||||
* .eq("name", "Rob")
|
|
||||||
* .filterMany("orders", "status = ?", Status.NEW)
|
|
||||||
* .findList();
|
|
||||||
*
|
|
||||||
* }</pre>
|
|
||||||
*
|
|
||||||
* @param manyProperty The many property
|
|
||||||
* @param expressions Filter expressions with and, or and ? or ?1 type bind parameters
|
|
||||||
* @param params Bind parameters used in the expressions
|
|
||||||
*/
|
|
||||||
@Deprecated(forRemoval = true)
|
|
||||||
ExpressionList<T> filterMany(String manyProperty, String expressions, Object... params);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add filter expressions for the many path. The expressions can include SQL functions if
|
* Add filter expressions for the many path. The expressions can include SQL functions if
|
||||||
* desired and the property names are translated to column names.
|
* desired and the property names are translated to column names.
|
||||||
@@ -604,19 +516,6 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
Query<T> setDistinct(boolean distinct);
|
Query<T> setDistinct(boolean distinct);
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the index(es) to search for a document store which uses partitions.
|
|
||||||
* <p>
|
|
||||||
* For example, when executing a query against ElasticSearch with daily indexes we can
|
|
||||||
* explicitly specify the indexes to search against.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param indexName The index or indexes to search against
|
|
||||||
* @return This query
|
|
||||||
* @see Query#setDocIndexName(String)
|
|
||||||
*/
|
|
||||||
Query<T> setDocIndexName(String indexName);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the first row to fetch.
|
* Set the first row to fetch.
|
||||||
*
|
*
|
||||||
@@ -700,14 +599,6 @@ public interface ExpressionList<T> {
|
|||||||
return setUseQueryCache(enabled ? CacheMode.ON : CacheMode.OFF);
|
return setUseQueryCache(enabled ? CacheMode.ON : CacheMode.OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set to true if this query should execute against the doc store.
|
|
||||||
* <p>
|
|
||||||
* When setting this you may also consider disabling lazy loading.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
Query<T> setUseDocStore(boolean useDocsStore);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set true if you want to disable lazy loading.
|
* Set true if you want to disable lazy loading.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -716,16 +607,6 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
Query<T> setDisableLazyLoading(boolean disableLazyLoading);
|
Query<T> setDisableLazyLoading(boolean disableLazyLoading);
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable read auditing for this query.
|
|
||||||
* <p>
|
|
||||||
* This is intended to be used when the query is not a user initiated query and instead
|
|
||||||
* part of the internal processing in an application to load a cache or document store etc.
|
|
||||||
* In these cases we don't want the query to be part of read auditing.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
Query<T> setDisableReadAuditing();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a label on the query (to help identify query execution statistics).
|
* Set a label on the query (to help identify query execution statistics).
|
||||||
*/
|
*/
|
||||||
@@ -745,14 +626,6 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
ExpressionList<T> where();
|
ExpressionList<T> where();
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the expressions to this expression list.
|
|
||||||
*
|
|
||||||
* @param expressions The expressions that are parsed and added to this expression list
|
|
||||||
* @param params Bind parameters to match ? or ?1 bind positions.
|
|
||||||
*/
|
|
||||||
ExpressionList<T> where(String expressions, Object... params);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path exists - for the given path in a JSON document.
|
* Path exists - for the given path in a JSON document.
|
||||||
* <pre>{@code
|
* <pre>{@code
|
||||||
@@ -1133,14 +1006,6 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
ExpressionList<T> like(String propertyName, String value);
|
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
|
* Case insensitive Like - property like value where the value contains the
|
||||||
* SQL wild card characters % (percentage) and _ (underscore). Typically uses
|
* SQL wild card characters % (percentage) and _ (underscore). Typically uses
|
||||||
@@ -1148,41 +1013,17 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
ExpressionList<T> ilike(String propertyName, String value);
|
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%.
|
* Starts With - property like value%.
|
||||||
*/
|
*/
|
||||||
ExpressionList<T> startsWith(String propertyName, String 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
|
* Case insensitive Starts With - property like value%. Typically uses a
|
||||||
* lower() function to make the expression case insensitive.
|
* lower() function to make the expression case insensitive.
|
||||||
*/
|
*/
|
||||||
ExpressionList<T> istartsWith(String propertyName, String value);
|
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.
|
* Ends With - property like %value.
|
||||||
*/
|
*/
|
||||||
@@ -1199,28 +1040,12 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
ExpressionList<T> contains(String propertyName, String value);
|
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()
|
* Case insensitive Contains - property like %value%. Typically uses a lower()
|
||||||
* function to make the expression case insensitive.
|
* function to make the expression case insensitive.
|
||||||
*/
|
*/
|
||||||
ExpressionList<T> icontains(String propertyName, String value);
|
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.
|
* In expression using pairs of value objects.
|
||||||
*/
|
*/
|
||||||
@@ -1695,47 +1520,6 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
ExpressionList<T> rawOrEmpty(String raw, Collection<?> values);
|
ExpressionList<T> rawOrEmpty(String raw, Collection<?> values);
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a match expression.
|
|
||||||
*
|
|
||||||
* @param propertyName The property name for the match
|
|
||||||
* @param search The search value
|
|
||||||
*/
|
|
||||||
ExpressionList<T> match(String propertyName, String search);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a match expression with options.
|
|
||||||
*
|
|
||||||
* @param propertyName The property name for the match
|
|
||||||
* @param search The search value
|
|
||||||
*/
|
|
||||||
ExpressionList<T> match(String propertyName, String search, Match options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a multi-match expression.
|
|
||||||
*/
|
|
||||||
ExpressionList<T> multiMatch(String search, String... properties);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a multi-match expression using options.
|
|
||||||
*/
|
|
||||||
ExpressionList<T> multiMatch(String search, MultiMatch options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a simple query string expression.
|
|
||||||
*/
|
|
||||||
ExpressionList<T> textSimple(String search, TextSimple options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a query string expression.
|
|
||||||
*/
|
|
||||||
ExpressionList<T> textQueryString(String search, TextQueryString options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add common terms expression.
|
|
||||||
*/
|
|
||||||
ExpressionList<T> textCommonTerms(String search, TextCommonTerms options);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* And - join two expressions with a logical and.
|
* And - join two expressions with a logical and.
|
||||||
*/
|
*/
|
||||||
@@ -1779,7 +1563,7 @@ public interface ExpressionList<T> {
|
|||||||
* .eq("status", Customer.Status.ACTIVE)
|
* .eq("status", Customer.Status.ACTIVE)
|
||||||
* .gt("id", 0)
|
* .gt("id", 0)
|
||||||
* .endAnd()
|
* .endAnd()
|
||||||
* .orderBy().asc("name")
|
* .order().asc("name")
|
||||||
* .findList();
|
* .findList();
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
@@ -1800,7 +1584,7 @@ public interface ExpressionList<T> {
|
|||||||
* .or()
|
* .or()
|
||||||
* .eq("status", Customer.Status.ACTIVE)
|
* .eq("status", Customer.Status.ACTIVE)
|
||||||
* .isNull("anniversary")
|
* .isNull("anniversary")
|
||||||
* .orderBy().asc("name")
|
* .order().asc("name")
|
||||||
* .findList();
|
* .findList();
|
||||||
*
|
*
|
||||||
* }</pre>
|
* }</pre>
|
||||||
@@ -1820,7 +1604,7 @@ public interface ExpressionList<T> {
|
|||||||
* .eq("status", Customer.Status.ACTIVE)
|
* .eq("status", Customer.Status.ACTIVE)
|
||||||
* .gt("id", 0)
|
* .gt("id", 0)
|
||||||
* .endAnd()
|
* .endAnd()
|
||||||
* .orderBy().asc("name")
|
* .order().asc("name")
|
||||||
* .findList();
|
* .findList();
|
||||||
*
|
*
|
||||||
* }</pre>
|
* }</pre>
|
||||||
@@ -1854,7 +1638,7 @@ public interface ExpressionList<T> {
|
|||||||
* .gt("id", 1)
|
* .gt("id", 1)
|
||||||
* .eq("anniversary", onAfter)
|
* .eq("anniversary", onAfter)
|
||||||
* .endNot()
|
* .endNot()
|
||||||
* .orderBy()
|
* .order()
|
||||||
* .asc("name")
|
* .asc("name")
|
||||||
* .findList();
|
* .findList();
|
||||||
*
|
*
|
||||||
@@ -1878,42 +1662,6 @@ public interface ExpressionList<T> {
|
|||||||
*/
|
*/
|
||||||
Junction<T> disjunction();
|
Junction<T> disjunction();
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a list of expressions that will be joined by MUST.
|
|
||||||
* <p>
|
|
||||||
* This automatically makes the query a useDocStore(true) query that
|
|
||||||
* will execute against the document store (ElasticSearch etc).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This is logically similar to and().
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
Junction<T> must();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a list of expressions that will be joined by SHOULD.
|
|
||||||
* <p>
|
|
||||||
* This automatically makes the query a useDocStore(true) query that
|
|
||||||
* will execute against the document store (ElasticSearch etc).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This is logically similar to or().
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
Junction<T> should();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a list of expressions that will be joined by MUST NOT.
|
|
||||||
* <p>
|
|
||||||
* This automatically makes the query a useDocStore(true) query that
|
|
||||||
* will execute against the document store (ElasticSearch etc).
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* This is logically similar to not().
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
Junction<T> mustNot();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* End a junction returning the parent expression list.
|
* End a junction returning the parent expression list.
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@@ -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;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
import io.ebean.service.SpiFetchGroupQuery;
|
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
|
* @param <T> The bean type the Fetch group can be applied to
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public interface FetchGroup<T> {
|
public interface FetchGroup<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,7 +84,7 @@ public interface FetchGroup<T> {
|
|||||||
* @return The FetchGroup with the given select clause
|
* @return The FetchGroup with the given select clause
|
||||||
*/
|
*/
|
||||||
static <T> FetchGroup<T> of(Class<T> cls, String select) {
|
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
|
* @return The FetchGroupBuilder with the given select clause which we will add fetch clauses to
|
||||||
*/
|
*/
|
||||||
static <T> FetchGroupBuilder<T> of(Class<T> cls) {
|
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.
|
* Return a query to be used by query beans for constructing FetchGroup.
|
||||||
*/
|
*/
|
||||||
static <T> SpiFetchGroupQuery<T> queryFor(Class<T> beanType) {
|
static <T> SpiFetchGroupQuery<T> queryFor(Class<T> beanType) {
|
||||||
return XBootstrapService.fetchGroupQueryFor(beanType);
|
return XServiceProvider.fetchGroupQueryFor(beanType);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a FetchGroup by adding fetch clauses.
|
* Builds a FetchGroup by adding fetch clauses.
|
||||||
@@ -23,7 +23,7 @@ import org.jspecify.annotations.NullMarked;
|
|||||||
*
|
*
|
||||||
* }</pre>
|
* }</pre>
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public interface FetchGroupBuilder<T> {
|
public interface FetchGroupBuilder<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -79,7 +79,7 @@ import java.util.Set;
|
|||||||
*
|
*
|
||||||
* @param <T> the entity bean type
|
* @param <T> the entity bean type
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public interface Filter<T> {
|
public interface Filter<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
package io.ebean;
|
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
|
||||||
import org.jspecify.annotations.Nullable;
|
|
||||||
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
|
||||||
import javax.sql.DataSource;
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common find operations shared by the query types that can execute and return
|
|
||||||
* results - {@link SqlQuery}, {@link DtoQuery}, {@link MappedQuery} and {@link QueryBuilder}.
|
|
||||||
*
|
|
||||||
* @param <SELF> The query type (used for method chaining)
|
|
||||||
* @param <T> The type of the result
|
|
||||||
*/
|
|
||||||
@NullMarked
|
|
||||||
public interface FindableQuery<SELF extends FindableQuery<SELF, T>, T> extends CancelableQuery {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query returning the list of results.
|
|
||||||
*/
|
|
||||||
List<T> findList();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query returning a single result, or {@code null} if there is no matching row.
|
|
||||||
* <p>
|
|
||||||
* If more than 1 row is found for this query then a PersistenceException is thrown.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
T findOne();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query returning an optional result.
|
|
||||||
*/
|
|
||||||
Optional<T> findOneOrEmpty();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query returning a single result or throwing a
|
|
||||||
* {@link jakarta.persistence.EntityNotFoundException} if there is no matching row.
|
|
||||||
*/
|
|
||||||
default T findOneOrThrow() {
|
|
||||||
return findOneOrEmpty().orElseThrow(() -> new EntityNotFoundException("Not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query returning a single result or throwing the exception produced
|
|
||||||
* by the given supplier if there is no matching row.
|
|
||||||
*/
|
|
||||||
default T findOneOrThrow(Supplier<? extends RuntimeException> exceptionSupplier) {
|
|
||||||
return findOneOrEmpty().orElseThrow(exceptionSupplier);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query using the given transaction.
|
|
||||||
*/
|
|
||||||
SELF usingTransaction(Transaction transaction);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the query using the given connection.
|
|
||||||
*/
|
|
||||||
SELF usingConnection(Connection connection);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure that the master DataSource is used if there is a read only data source
|
|
||||||
* being used (that is using a read replica database potentially with replication lag).
|
|
||||||
* <p>
|
|
||||||
* When the database is configured with a read-only DataSource via
|
|
||||||
* say {@link DatabaseBuilder#readOnlyDataSource(DataSource)} then
|
|
||||||
* by default when a query is run without an active transaction, it uses the read-only data
|
|
||||||
* source. 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);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package io.ebean;
|
package io.ebean;
|
||||||
|
|
||||||
import org.jspecify.annotations.NullMarked;
|
import io.avaje.lang.NonNullApi;
|
||||||
import org.jspecify.annotations.Nullable;
|
import io.avaje.lang.Nullable;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,7 +35,7 @@ import java.util.List;
|
|||||||
* public List<Customer> findNew() {
|
* public List<Customer> findNew() {
|
||||||
* return query().where()
|
* return query().where()
|
||||||
* .eq("status", Customer.Status.NEW)
|
* .eq("status", Customer.Status.NEW)
|
||||||
* .orderBy("name")
|
* .order("name")
|
||||||
* .findList()
|
* .findList()
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
@@ -60,7 +60,7 @@ import java.util.List;
|
|||||||
* @see BeanRepository
|
* @see BeanRepository
|
||||||
* @see BeanFinder
|
* @see BeanFinder
|
||||||
*/
|
*/
|
||||||
@NullMarked
|
@NonNullApi
|
||||||
public class Finder<I, T> {
|
public class Finder<I, T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,11 +213,4 @@ public class Finder<I, T> {
|
|||||||
return db().findNative(type, nativeSql);
|
return db().findNative(type, nativeSql);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a query using the ORM query language.
|
|
||||||
*/
|
|
||||||
public Query<T> query(String ormQuery) {
|
|
||||||
return db().createQuery(type, ormQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package io.ebean;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FutureIds represents the result of a background query execution for the Id's.
|
|
||||||
* <p>
|
|
||||||
* It extends the java.util.concurrent.Future with the ability to get the Id's
|
|
||||||
* while the query is still executing in the background.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
public interface FutureIds<T> extends Future<List<Object>> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the original query used to fetch the Id's.
|
|
||||||
*/
|
|
||||||
Query<T> getQuery();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user