Compare commits

..
Author SHA1 Message Date
rob bygrave 08d241d946 #1966 Remove sequence batching - postgres opps 2020-03-05 00:34:05 +13:00
rob bygrave 1c24ebf6dc Remove Sequence batching in favour of JPA increment 50
Also remove h2sqldb in favour of always using H2
2020-03-05 00:22:20 +13:00
6058 changed files with 183826 additions and 301444 deletions
+6
View File
@@ -1,3 +1,9 @@
GITHUB ISSUES ARE STRICTLY CONTROLLED FOR THIS PROJECT.
Refer to http://ebean-orm.github.io/support for the policies controlling the use of github issues.
Please post issues to the Ebean group https://groups.google.com/forum/#!forum/ebean first.
## Expected behavior
## Actual behavior
-46
View File
@@ -1,46 +0,0 @@
name: Build
on:
workflow_dispatch:
pull_request:
push:
branches: master
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'zulu'
- 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: Maven single test
# run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false
- name: Build with Maven
run: mvn -T 1C clean install -Pdefault
- name: Test SequencedSet and SequencedMap (requires installed MR-JAR)
run: cd tests/test-java16 && mvn test
-38
View File
@@ -1,38 +0,0 @@
name: DB2 LUW
on:
workflow_dispatch:
schedule:
- cron: '10 8 * * 2,5'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: db2
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-db2.properties
-41
View File
@@ -1,41 +0,0 @@
name: H2Database
on:
workflow_dispatch:
schedule:
- cron: '10 1 * * *'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'zulu'
- 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: H2Database
run: mvn -T 1C clean package
-41
View File
@@ -1,41 +0,0 @@
name: ebean EA
on:
workflow_dispatch:
schedule:
- cron: '39 6 * * 1,3,5'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [GA,EA]
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: Build with Maven
run: mvn test -Pea
-38
View File
@@ -1,38 +0,0 @@
name: MariaDB
on:
workflow_dispatch:
schedule:
- cron: '10 7 * * 1,4'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: mariadb 10.11
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mariadb.properties
-51
View File
@@ -1,51 +0,0 @@
name: Multi-database platform
on: [workflow_dispatch]
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: h2database
run: mvn clean test
- name: postgres
run: mvn clean test -Dprops.file=testconfig/ebean-postgres.properties
- name: mysql
run: mvn clean test -Dprops.file=testconfig/ebean-mysql.properties
- name: mariadb
run: mvn clean test -Dprops.file=testconfig/ebean-mariadb.properties
- name: yugabyte
run: mvn clean test -Dprops.file=testconfig/ebean-yugabyte.properties
- name: sqlserver
run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver17.properties
# - name: sqlserver19
# run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver19.properties
# - name: db2
# run: mvn clean test -Dprops.file=testconfig/ebean-db2.properties
# - name: oracle
# run: mvn clean test -Dprops.file=testconfig/ebean-oracle.properties
-41
View File
@@ -1,41 +0,0 @@
name: Multi-JDK Build
on:
workflow_dispatch:
schedule:
- cron: '30 6 * * *'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'zulu'
- 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: Build with Maven
run: mvn package
-38
View File
@@ -1,38 +0,0 @@
name: MySql
on:
workflow_dispatch:
schedule:
- cron: '20 6 * * *'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: mysql
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-mysql.properties
-38
View File
@@ -1,38 +0,0 @@
name: Oracle
on:
workflow_dispatch:
schedule:
- cron: '10 6 * * 1,4'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'zulu'
- name: Maven cache
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: oracle
run: mvn -T 1 clean test -Dprops.file=testconfig/ebean-oracle.properties
-38
View File
@@ -1,38 +0,0 @@
name: Postgres
on:
workflow_dispatch:
schedule:
- cron: '10 6 * * *'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: postgres
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-postgres.properties
-35
View File
@@ -1,35 +0,0 @@
name: SqlServer 2019
on: [workflow_dispatch]
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: sqlserver 2019 latest
run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver19.properties
-38
View File
@@ -1,38 +0,0 @@
name: SqlServer
on:
workflow_dispatch:
schedule:
- cron: '10 7 * * 2,5'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: sqlserver 2022
run: mvn -T 1C clean test -Dprops.file=testconfig/ebean-sqlserver.properties
-41
View File
@@ -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
-38
View File
@@ -1,38 +0,0 @@
name: Yugabyte
on:
workflow_dispatch:
schedule:
- cron: '10 3 * * 3'
jobs:
build:
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
java_version: [21]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java_version }}
distribution: 'adopt'
- name: Maven cache
uses: actions/cache@v4
env:
cache-name: maven-cache
with:
path:
~/.m2
key: build-${{ env.cache-name }}
- name: yugabyte
run: mvn clean test -Dprops.file=testconfig/ebean-yugabyte.properties
+4 -5
View File
@@ -1,4 +1,6 @@
*.autofetch
*create-all.sql
*drop-all.sql
*.orig
.classpath
.project
@@ -10,15 +12,12 @@ ebean-autotune.xml
ebean-profiling*.xml
/db
/mydb.db
!src/test/ddl-review/*.sql
profiling/
.DS_Store
# Local Redis integration test credentials
ebean-redis/src/test/resources/redis-local.yml
# Intellij project files
*.iml
*.ipr
*.iws
.idea/
*uuid.state
*uuid.state
+1 -1
View File
@@ -34,7 +34,7 @@ env:
install: true
script:
- mvn -T 1C clean test
- mvn clean test
after_failure:
- ./.travis/print_surefire_reports
+1 -8
View File
@@ -7,11 +7,4 @@ do
echo ${file}
cat ${file}
echo
done
for file in ebean-autotune/target/surefire-reports/*.txt
do
echo ${file}
cat ${file}
echo
done
done
+13 -12
View File
@@ -4,13 +4,13 @@ Overview of ebean.properties file
### DbMigration options
You can set individual values for different platforms when generating migrations.
You can set individual values for different platforms when generating migrations
`dbmigration.platform.<PLATFORM>.databaseSequenceBatchSize`
For DB's using sequences this is the number of sequence values prefetched.
`dbmigration.platform.<PLATFORM>.dbuuid`
Control, how UUID generation should work - it affects which column type is generated in DDL. Possible values:
Control, how UUID generation should work - it affects DDL which column type is generated. Possible values:
- BINARY enforces binary UUID
- VARCHAR enforces varchar UUID
- BINARY_OPTIMIZED enforces binary-optimized UUID (makes sense only with Type1 ID)
@@ -19,7 +19,7 @@ Control, how UUID generation should work - it affects which column type is gener
- AUTO_VARCHAR (default) use varchar when platform does not support UUID
`dbmigration.platform.<PLATFORM>.uuidStoreAsBinary`
Same as setting dbuuid to BINARY.
Same as setting dbuuid to BINARY
`dbmigration.platform.<PLATFORM>.geometrySRID`
The Geometry SRID value (default 4326).
@@ -30,6 +30,7 @@ The ID type (IDENTITY, SEQUENCE, GENERATOR, EXTERNAL)
`dbmigration.platform.<PLATFORM>.mapping`
Adjust the mapping. For example `BOOLEAN=integer(32);BIT=tinyint(3)`
`ebean.migration.applyPrefix`
Set this to "V" to be compatible with FlywayDB.
@@ -49,7 +50,7 @@ Set to true if the DB migration should be generated on server start.
The version of a pending drop that should be generated as the next migration.
`ebean.migration.includeGeneratedFileComment`
Adds the header about the migration files being generated when true. Reading 'THIS IS A GENERATED FILE - DO NOT MODIFY'
TODO
`ebean.migration.metaTable`
For running migration the DB table that holds migration execution status. Default 'db_migration'
@@ -64,17 +65,17 @@ Subdirectory the model xml files go into. Default 'model'
Suffix. Default '.model.xml'
`ebean.migration.name`
Description text that can be appended to the version to become the ddl script file name.
Description text that can be appended to the version to become the ddl script file name
`ebean.migration.patchInsertOn`
Migration versions that should be added to history without running.
migration versions that should be added to history without running.
`ebean.migration.patchResetChecksumOn`
migration versions that should have their checksum reset and not run.
Use this if you get a 'Checksum mismatch' error.
`ebean.migration.placeholders`
A comma and equals delimited map of placeholders that are substituted in SQL scripts when running migration (used by DB Migration runner only).
A comma and equals delimited placeholders that are substituted in SQL scripts when running migration (used by DB Migration runner only).
`ebean.migration.platform`
The database platform to generate migration DDL for.
@@ -89,7 +90,7 @@ The migration version name (typically FlywayDb compatible). Example: 1.1.1_2
### Ebean UUID options
`ebean.uuidVersion`
Controls how the UUIDs are generated. Possible values:
Controls, how the UUIDs are generated. Possible values
- VERSION4 (default) generate random V4 UUIDs,
- VERSION1 generate rfc4122 compliant Type 1 UUIDs (requires a state file)
- VERSION1RND generate fake Type 1 UUIDs
@@ -97,7 +98,7 @@ Controls how the UUIDs are generated. Possible values:
Note, that V1 UUIDs in conjunction with AUTO_BINARY_OPTIMIZED will give you the best index performance, but you MUST understand how this works to avoid collisions.
`ebean.uuidStateFile`
The state file that is required to generate V1 UUIDs.
The state file that is Required to generate V1 UUIDs
### DocStoreConfig
@@ -106,7 +107,7 @@ The state file that is required to generate V1 UUIDs.
True when the Document store integration is active/on.
`ebean.docstore.allowAllCertificates`
Set to true such that the client allows connections to invalid/self-signed SSL certificates.
Set to true such that the client allows connections to invalid/self signed SSL certificates.
`ebean.docstore.bulkBatchSize`
The default batch size to use for the Bulk API calls.
@@ -191,7 +192,7 @@ Suffix appended to the base table to derive the view that contains the union of
Set to true if the DataSource uses autoCommit. Indicates that Ebean should use autoCommit friendly Transactions and TransactionManager.
`ebean.autoReadOnlyDataSource`
When true create a read only DataSource using readOnlyDataSourceConfig defaulting values from dataSourceConfig.
When true create a read only DataSource using readOnlyDataSourceConfig defaulting values from dataSourceConfig
`ebean.autostart`
Should the server start all
@@ -292,6 +293,6 @@ ebean.tenant.schemaProvider
ebean.updateAllPropertiesInBatch
ebean.updateChangesOnly
ebean.updatesDeleteMissingChildren
ebean.useValidationNotNull
ebean.useJavaxValidationNotNull
ebean.useJtaTransactionManager
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2012 Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+15
View File
@@ -0,0 +1,15 @@
EbeanORM
Copyright 2012 Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+16 -101
View File
@@ -1,63 +1,18 @@
[![Build](https://github.com/ebean-orm/ebean/actions/workflows/build.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/build.yml)
[![Build Status](https://travis-ci.org/ebean-orm/ebean.svg?branch=master)](https://travis-ci.org/ebean-orm/ebean)
[![Maven Central : ebean](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ebean-orm/ebean/blob/master/LICENSE)
[![Multi-JDK Build](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml)
[![GraalVM Native Image](https://img.shields.io/badge/GraalVM-Native%20Image%20Ready-darkgreen?logo=graalvm)](https://www.graalvm.org/)
##### Build with database platforms
[![H2Database](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml)
[![Postgres](https://github.com/ebean-orm/ebean/actions/workflows/postgres.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/postgres.yml)
[![MySql](https://github.com/ebean-orm/ebean/actions/workflows/mysql.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/mysql.yml)
[![MariaDB](https://github.com/ebean-orm/ebean/actions/workflows/mariadb.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/mariadb.yml)
[![Oracle](https://github.com/ebean-orm/ebean/actions/workflows/oracle.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/oracle.yml)
[![SqlServer](https://github.com/ebean-orm/ebean/actions/workflows/sqlserver.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/sqlserver.yml)
[![DB2 LUW](https://github.com/ebean-orm/ebean/actions/workflows/db2luw.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/db2luw.yml)
[![Yugabyte](https://github.com/ebean-orm/ebean/actions/workflows/yugabyte.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/yugabyte.yml)
##### Build with Java Early Access versions
[![ebean EA](https://github.com/ebean-orm/ebean/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/jdk-ea.yml)
[![datasource EA](https://github.com/ebean-orm/ebean-datasource/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-datasource/actions/workflows/jdk-ea.yml)
[![migration EA](https://github.com/ebean-orm/ebean-migration/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-migration/actions/workflows/jdk-ea.yml)
[![test-docker EA](https://github.com/ebean-orm/ebean-test-docker/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-test-docker/actions/workflows/jdk-ea.yml)
[![ebean-agent EA](https://github.com/ebean-orm/ebean-agent/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-agent/actions/workflows/jdk-ea.yml)
----------------------
# Ebean ORM for Java & Kotlin
**Multiple abstraction levels**: Ebean provides multiple levels of query abstraction [ORM Queries, mixed with SQL](https://ebean.io/docs/intro/queries/orm-query), [DTO Queries](https://ebean.io/docs/intro/queries/dto-query), [SqlQuery and JDBC](https://ebean.io/docs/intro/queries/sql-query).
Work at the highest level of abstraction and drop down levels as needed.
**Database migrations**: Built in [DB migration](https://ebean.io/docs/db-migrations/) generation and running. Support for "rebase" migrations as well as repeatable, init and 'normal' migrations.
**Awesome SQL**: Ebean produces SQL that you would hand craft yourself. Use great SQL, never generate SQL cartesian product, always honor relational limit/offset.
**Automated query tuning**: For ORM queries Ebean can profile the object graph being used and either [automatically tune the query](https://ebean.io/docs/query/background/autotune).
**Docker test containers**: [Docker test containers](https://ebean.io/docs/testing/) for all the supported databases. Get 100% test coverage on all the features of the database we use.
**Type safe queries**: We can build queries using type safe [query beans](https://ebean.io/docs/query/query-beans). IDE auto-complete when writing queries, compile time checking and it's FUN.
**Performance isn't optional**: Optimise queries to only fetch what we need (partial objects). Automatically avoid N+1 via a smart load context.
#### Benefits of ORM
* Automatically avoid N+1
* L2 caching to reduce database load
* Queries mixing database and L2 cache
* Automatically tune ORM queries
* Elasticsearch for search or L3 cache
----------------------
# Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://www.foconis.de/" target="_blank">
<img width="222px" src="https://group.foconis.com/download/ci/logo/png-72dpi/logo-quer/foconis-analytics-quer.png">
<img width="222px" src="https://www.foconis.de/templates/yootheme/cache/foconis_logo_322-709da1de.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.payintech.com/" target="_blank">
<img width="222px" src="https://ebean.io/images/sponsor_PayinTech-logo-noir.png">
</a>
</td>
<td align="center" valign="middle">
@@ -74,58 +29,18 @@ Work at the highest level of abstraction and drop down levels as needed.
</tbody>
</table>
## Need help?
Post questions or issues to the [Ebean google group](https://groups.google.com/forum/#!forum/ebean)
or [github discussions](https://github.com/ebean-orm/ebean/discussions)
# Need help?
Post questions or issues to the Ebean google group - https://groups.google.com/forum/#!forum/ebean
## Documentation
# Documentation
Goto [https://ebean.io/docs/](https://ebean.io/docs/)
## Guides
Library reference (capabilities, scope, and AI guidance): [docs/LIBRARY.md](docs/LIBRARY.md)
Step-by-step guides for common tasks: [docs/guides/](docs/guides/README.md)
## Maven cental links:
[Maven central - ebean](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.ebean%22%20AND%20a%3A%22ebean%22 "maven central ebean")
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 - all related projects](http://search.maven.org/#search%7Cga%7C1%7Cebean "maven central all related projects")
## Maven central
[Maven central - g:io.ebean](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.ebean%22%20)
## Building Ebean from source
- JDK 11 or higher installed
- Maven installed
- `git clone git@github.com:ebean-orm/ebean.git`
- `mvn clean install`
Ebean 13 uses Java modules with module-info. This means that there are stricter compilation
rules in place now than when building with classpath pre version 13.
For Maven Surefire testing we use `<surefire.useModulePath>false</surefire.useModulePath>` such
that tests run using classpath and not module-path. We are doing this until all the tooling
(Maven, IDE) improves in the area of testing with module-info.
#### Eclipse IDE
Right now we can't use Eclipse IDE to build Ebean and run its tests due to its poor support
for java modules. See [ebean/issues/2653](https://github.com/ebean-orm/ebean/issues/2653)
The current recommendation is to use IntelliJ IDEA as the IDE to build and hack Ebean.
#### IntelliJ IDEA
We want to get IntelliJ to run tests using classpath similar to Maven Surefire. To do this set:
`JUnit -> modify options -> Do not use module-path option`
To set this option as the global default for IntelliJ use:
`Run - Edit Configurations -> Edit configuration templates -> JUnit -> modify options - Do not use module-path option`
end
## Other versions
* [![Maven Central : ebean-agent](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean-agent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean-agent) - ebean-agent
* [![Maven Central : ebean-maven-plugin](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean-maven-plugin/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean-maven-plugin) - ebean-maven-plugin
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-clickhouse</name>
<description>ebean-clickhouse composite</description>
<artifactId>ebean-clickhouse</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-clickhouse</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.clickhouse.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.clickhouse {
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.clickhouse;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-cockroach</name>
<description>ebean-cockroach composite</description>
<artifactId>ebean-cockroach</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.cockroach.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.cockroach {
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;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-db2</name>
<description>ebean-db2 composite</description>
<artifactId>ebean-db2</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-db2</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.db2.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.db2 {
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.db2;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-h2</name>
<description>ebean-h2 composite</description>
<artifactId>ebean-h2</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-h2</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.h2.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.h2 {
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.h2;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-hana</name>
<description>ebean-hana composite</description>
<artifactId>ebean-hana</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-hana</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.hana.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.hana {
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.hana;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-mariadb</name>
<description>ebean-mariadb composite</description>
<artifactId>ebean-mariadb</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mariadb</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.mariadb.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.mariadb {
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.mariadb;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-mysql</name>
<description>ebean-mysql composite</description>
<artifactId>ebean-mysql</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-mysql</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.mysql.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.mysql {
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.mysql;
}
-85
View File
@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-net-postgis</name>
<description>ebean-net-postgis composite</description>
<artifactId>ebean-net-postgis</artifactId>
<properties>
<postgis.jdbc.version>2023.1.0</postgis.jdbc.version>
<postgres.jdbc.version>42.7.11</postgres.jdbc.version>
</properties>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-net-postgis-types</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgres.jdbc.version}</version>
<exclusions>
<!-- exclude unnecessary checker framework -->
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
<version>${postgis.jdbc.version}</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.postgis.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,10 +0,0 @@
module io.ebean.postgis {
requires transitive io.ebean.api;
requires transitive io.ebean.core;
requires transitive io.ebean.datasource;
requires transitive io.ebean.querybean;
requires transitive io.ebean.platform.postgres;
// requires transitive io.ebean.postgis.types;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-nuodb</name>
<description>ebean-nuodb composite</description>
<artifactId>ebean-nuodb</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-nuodb</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.nuodb.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.nuodb {
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.nuodb;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-oracle</name>
<description>ebean-oracle composite</description>
<artifactId>ebean-oracle</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-oracle</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.oracle.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.oracle {
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.oracle;
}
-85
View File
@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-pgvector</name>
<description>ebean-pgvector composite</description>
<artifactId>ebean-pgvector</artifactId>
<properties>
<pgvector.version>0.1.6</pgvector.version>
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
</properties>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-pgvector-types</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgres.jdbc.version}</version>
<exclusions>
<!-- exclude unnecessary checker framework -->
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.pgvector</groupId>
<artifactId>pgvector</artifactId>
<version>${pgvector.version}</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.pgvector.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.pgvector {
requires transitive io.ebean.api;
requires transitive io.ebean.core;
requires transitive io.ebean.datasource;
requires transitive io.ebean.querybean;
requires transitive io.ebean.platform.postgres;
}
-85
View File
@@ -1,85 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-postgis</name>
<description>ebean-postgis composite</description>
<artifactId>ebean-postgis</artifactId>
<properties>
<postgis.jdbc.version>2.5.1</postgis.jdbc.version>
<postgres.jdbc.version>42.7.2</postgres.jdbc.version>
</properties>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgis-types</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgres.jdbc.version}</version>
<exclusions>
<!-- exclude unnecessary checker framework -->
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>net.postgis</groupId>
<artifactId>postgis-jdbc</artifactId>
<version>${postgis.jdbc.version}</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.postgis.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,10 +0,0 @@
module io.ebean.postgis {
requires transitive io.ebean.api;
requires transitive io.ebean.core;
requires transitive io.ebean.datasource;
requires transitive io.ebean.querybean;
requires transitive io.ebean.platform.postgres;
// requires transitive io.ebean.postgis.types;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-postgres</name>
<description>ebean-postgres composite</description>
<artifactId>ebean-postgres</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.postgres.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.postgres {
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;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-sqlite</name>
<description>ebean-sqlite composite</description>
<artifactId>ebean-sqlite</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlite</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.sqlite.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.sqlite {
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.sqlite;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-sqlserver</name>
<description>ebean-sqlserver composite</description>
<artifactId>ebean-sqlserver</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-sqlserver</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.sqlserver.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.sqlserver {
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.sqlserver;
}
-55
View File
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean-yugabyte</name>
<description>ebean-yugabyte composite</description>
<artifactId>ebean-yugabyte</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-postgres</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.yugabyte.assembly;
/**
* Nothing interesting here - required placeholder for javadoc.
*/
public class Assembly {
}
@@ -1,9 +0,0 @@
module io.ebean.yugabyte {
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;
}
-73
View File
@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
<relativePath>../..</relativePath>
</parent>
<name>ebean (all platforms)</name>
<description>composite of common runtime dependencies for all platforms</description>
<artifactId>ebean</artifactId>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-api</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-core</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-joda-time</artifactId>
<version>14.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-jsonnode</artifactId>
<version>14.0.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-jackson-mapper</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean-migration.version}</version>
</dependency>
<!-- Technically optional but most expected to use query beans -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-querybean</artifactId>
<version>18.2.0</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-platform-all</artifactId>
<version>18.2.0</version>
</dependency>
</dependencies>
</project>
@@ -1,7 +0,0 @@
package io.ebean.assembly;
/**
* Nothing interesting here - look at ebean-api.
*/
public class Assembly {
}
@@ -1,8 +0,0 @@
module io.ebean {
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.all;
}
-35
View File
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
</parent>
<artifactId>composites</artifactId>
<packaging>pom</packaging>
<modules>
<module>ebean</module>
<module>ebean-clickhouse</module>
<module>ebean-cockroach</module>
<module>ebean-db2</module>
<module>ebean-h2</module>
<module>ebean-hana</module>
<!-- <module>hsqldb</module>-->
<module>ebean-mariadb</module>
<module>ebean-mysql</module>
<module>ebean-nuodb</module>
<module>ebean-oracle</module>
<module>ebean-postgres</module>
<module>ebean-postgis</module>
<module>ebean-net-postgis</module>
<module>ebean-pgvector</module>
<!-- <module>sqlanywhere</module>-->
<module>ebean-sqlite</module>
<module>ebean-sqlserver</module>
<module>ebean-yugabyte</module>
</modules>
</project>
-250
View File
@@ -1,250 +0,0 @@
# Ebean ORM Library Definition
Ebean is an ORM library for Java and Kotlin focused on relational data access, type-safe query construction, and production-friendly SQL behavior.
## Identity
- **Name**: Ebean ORM
- **Package**: `io.ebean`
- **Primary Maven Group**: `io.ebean`
- **Category**: ORM / Data Access
- **Repository**: https://github.com/ebean-orm/ebean
- **Issues**: https://github.com/ebean-orm/ebean/issues
- **Discussions**: https://github.com/ebean-orm/ebean/discussions
- **Website**: https://ebean.io/
- **Documentation**: https://ebean.io/docs/
- **License**: Apache 2.0
## Version & Requirements
- **Repository Version (this checkout)**: `16.5.0` (from repository `pom.xml`)
- **Minimum Java Version**: 11+
- **Languages**: Java, Kotlin
- **Build Tooling in this docs set**: Maven-focused examples
## Core Artifacts
| Artifact | Purpose |
|------|------|
| `io.ebean:ebean` | Core ORM runtime and API |
| `io.ebean:ebean-postgres` | PostgreSQL platform bundle used in setup guides |
| `io.ebean:ebean-test` | Test support, including Docker-backed database testing |
| `io.ebean:querybean-generator` | Generates `Q*` type-safe query beans |
| `io.ebean:ebean-maven-plugin` | Bytecode enhancement for entities at build time |
| `io.ebean:ebean-migration` | Runtime migration runner (often transitive via platform artifact) |
## Core APIs & Annotations
### Database and transaction APIs
| API | Purpose | Example |
|------|------|------|
| `DB.getDefault()` | Access default `Database` | `Database db = DB.getDefault();` |
| `DB.byName("...")` | Access named `Database` | `Database reporting = DB.byName("reporting");` |
| `database.find(...)` | Query entities | `Customer c = database.find(Customer.class, id);` |
| `database.insert/save/update/delete` | Persist entity changes | `database.save(customer);` |
| `database.beginTransaction()` | Manual transaction boundary | `try (Transaction txn = database.beginTransaction()) { ... }` |
| `Database.builder()` | Programmatic `Database` setup | `Database.builder().loadFromProperties().build();` |
### Query APIs
| API | Purpose | Example |
|------|------|------|
| `Q*` query beans | Type-safe query construction | `new QCustomer().status.equalTo(ACTIVE).findList();` |
| `exists()` | Efficient existence checks | `new QCustomer().email.equalTo(email).exists();` |
| `findOne()` | Unique/single-row retrieval | `new QCustomer().id.equalTo(id).findOne();` |
| `findList()` | List retrieval | `new QCustomer().findList();` |
| `asDto(...).findList()` | DTO projection reads | `new QOrder().asDto(OrderSummary.class).findList();` |
### Entity mapping and lifecycle annotations
| Annotation | Purpose |
|------|------|
| `@Entity` | Marks class as persistent entity |
| `@Id` | Primary key mapping |
| `@Version` | Optimistic locking |
| `@WhenCreated` | Creation timestamp management |
| `@WhenModified` | Modification timestamp management |
| `@Transactional` | Declarative transaction boundary |
## Capabilities
### ✅ Included
- Relational ORM with automatic dirty checking and lazy loading (via enhancement)
- Multiple query abstraction levels (ORM query, DTO query, SQL/JDBC)
- Type-safe query beans (`Q*`) with IDE autocomplete
- Built-in migration generation and migration running support
- Transaction APIs for implicit, declarative, and explicit transaction control
- Support for test-time Docker database workflows
- Query tuning and caching features for performance-sensitive workloads
### ❌ Not in scope
- HTTP routing, REST controllers, or web server runtime
- Dependency injection container functionality
- JSON serialization framework responsibilities
- Front-end/UI rendering concerns
Ebean is intentionally focused on persistence and data access. Pair it with a web framework and DI library as needed.
## Use Cases
### ✅ Strong fit
- SQL-backed business applications with rich domain models
- Services that need both ORM productivity and SQL-level control
- Projects requiring type-safe query authoring via generated query beans
- Teams that want migration generation integrated with entity model changes
- Integration test suites that need real database behavior (not only in-memory mocks)
### ⚠️ Consider alternatives if
- You need a full web framework (routing/controllers) rather than a persistence layer
- Your project does not use relational databases as a core storage model
- You want a single library to cover persistence, DI, and HTTP all at once
## Quick Start (Maven)
```xml
<properties>
<ebean.version><!-- use latest stable from Maven Central --></ebean.version>
</properties>
<dependencies>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>${ebean.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>${ebean.version}</version>
<extensions>true</extensions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>${ebean.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
```
## Minimal Example
```java
import io.ebean.DB;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
class Customer {
@Id
private long id;
private String name;
public void setName(String name) {
this.name = name;
}
}
Database database = DB.getDefault(); // or injected
Customer customer = database.find(Customer.class, 42);
customer.setName("Updated");
database.save(customer);
```
## Common Tasks & Guides
| Task | Guide |
|------|------|
| Add Ebean to an existing Maven project | [add-ebean-postgres-maven-pom.md](guides/add-ebean-postgres-maven-pom.md) |
| Configure database and `Database` bean | [add-ebean-postgres-database-config.md](guides/add-ebean-postgres-database-config.md) |
| Add PostgreSQL test container support | [add-ebean-postgres-test-container.md](guides/add-ebean-postgres-test-container.md) |
| Generate DB migrations | [add-ebean-db-migration-generation.md](guides/add-ebean-db-migration-generation.md) |
| Migrate JSON APIs from Jackson core to avaje-json-core | [migrating-json-jackson-core-to-avaje-json-core.md](guides/migrating-json-jackson-core-to-avaje-json-core.md) |
| Know which `@DbJson` types need Jackson vs built-in | [dbjson-mapping-support.md](guides/dbjson-mapping-support.md) |
| Model entity beans correctly | [entity-bean-creation.md](guides/entity-bean-creation.md) |
| Use Lombok safely with entities | [lombok-with-ebean-entity-beans.md](guides/lombok-with-ebean-entity-beans.md) |
| Write type-safe query bean queries | [writing-ebean-query-beans.md](guides/writing-ebean-query-beans.md) |
| Persist changes and manage transactions | [persisting-and-transactions-with-ebean.md](guides/persisting-and-transactions-with-ebean.md) |
| Build test entities quickly | [testing-with-testentitybuilder.md](guides/testing-with-testentitybuilder.md) |
**Guides index**: [guides/README.md](guides/README.md)
## Related Ecosystem Docs
- [Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md)
- [AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md)
- [Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md)
## AI Agent Instructions
### For Claude, GPT, and web-based agents
Use this file as the top-level reference when answering Ebean questions.
1. Check this file first for scope and capability fit.
2. Route implementation tasks to the relevant guide in **Common Tasks & Guides**.
3. Treat Ebean as the persistence layer only; avoid implying it provides HTTP/DI features.
4. Prefer type-safe query bean examples when showing query code.
5. For setup and migration changes, follow the Maven-focused guide steps exactly.
### For IDE-based agents (Copilot, Cursor, etc.)
If `docs/LIBRARY.md` is not in context automatically:
1. Read `README.md` for docs entry points.
2. Open `docs/guides/README.md` for task-specific guides.
3. Follow linked guide files directly for concrete implementation steps.
---
## Notes for Maintainers
### When to update this file
- New release that changes requirements or key APIs
- New guide added to `docs/guides/`
- Capability/scope changes that affect "Included" or "Not in scope"
- Significant migration or setup workflow changes
### Maintenance checklist
- [ ] Keep requirements and version references accurate
- [ ] Keep Common Tasks table aligned with `docs/guides/README.md`
- [ ] Keep artifact names/snippets aligned with setup guides
- [ ] Keep AI instructions aligned with current docs structure
### Link from repository README
In `README.md`, include:
```markdown
## Documentation
- [Ebean docs](https://ebean.io/docs/)
- [Library reference](docs/LIBRARY.md)
- [Step-by-step guides](docs/guides/README.md)
```
-25
View File
@@ -1,25 +0,0 @@
# AGENTS.md — Ebean ORM
This project uses [Ebean ORM](https://ebean.io).
## AI Agent Instructions
Step-by-step guides for common Ebean tasks (Maven setup, database config, query writing, transactions, test containers, DB migrations, Lombok) are at:
**https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/**
Key guides (fetch and follow when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Migrate JSON APIs from Jackson core to avaje-json-core: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-json-jackson-core-to-avaje-json-core.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Derived / formula properties (`@Formula`, `@Formula2`): https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/derived-formula-properties.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
Agents: Before performing any Ebean-related task, fetch and follow the relevant guide above.
-205
View File
@@ -1,205 +0,0 @@
# Guides
See also: [AGENTS.md](AGENTS.md) — a minimal template for AI agent onboarding and automation in Ebean ORM projects.
Step-by-step guides written as instructions for AI agents and developers.
For a high-level capability reference (scope, core APIs, and AI guidance), see
[../LIBRARY.md](../LIBRARY.md).
## Adding Ebean ORM with PostgreSQL to an existing Maven project
A three-part guide covering everything needed to wire Ebean + PostgreSQL into an
existing Maven project. Complete the steps in order.
| Step | Guide | Description |
|------|-------|-------------|
| 1 | [Maven POM setup](add-ebean-postgres-maven-pom.md) | Add Ebean dependencies, the enhancement plugin, and the querybean-generator annotation processor to `pom.xml` |
| 2 | [Test container setup](add-ebean-postgres-test-container.md) | Start a PostgreSQL (or PostGIS) Docker container for tests using `@TestScope @Factory` with Avaje Inject; verify the test database works with `mvn verify` before adding production configuration |
| 3 | [Database configuration](add-ebean-postgres-database-config.md) | Configure the production Ebean `Database` bean using `DataSourceBuilder` and `DatabaseBuilder` with Avaje Inject |
## Migration & upgrades
| Guide | Description |
|-------|-------------|
| [Migrate to `Database.builder()`](migrating-to-database-builder.md) | Replace legacy `new DatabaseConfig()` and `DatabaseFactory.create(...)` code with `Database.builder()` and `DatabaseBuilder.build()`. Includes common rewrites, fluent builder equivalents, and manual-review cases for semi-automated upgrades |
| [Migrate JSON APIs from Jackson core to avaje-json-core](migrating-json-jackson-core-to-avaje-json-core.md) | Cut over `JsonParser`/`JsonGenerator`/`JsonFactory` usage to `JsonReader`/`JsonWriter`/`JsonStream`, including `DatabaseBuilder`/`DatabaseConfig` JSON config changes and validation checklist |
## Observability
| Guide | Description |
|-------|-------------|
| [Ebean OpenTelemetry tracing](add-ebean-opentelemetry.md) | Add `ebean-opentelemetry`, register `GlobalOpenTelemetry` once before Ebean databases are built, and troubleshoot missing spans or double-registration errors |
| [Ebean query metrics and naming](ebean-query-metrics.md) | How Ebean query metric names are derived from `setLabel(..)` and profile locations; secondary (lazy/query) load naming; inline SQL comments; collecting metrics at runtime; mapping to avaje-metrics tags |
| [Ebean query plan capture](ebean-query-plan-capture.md) | Enable and configure database query plan (`EXPLAIN`) capture for slow queries; bind capture vs plan capture; periodic and on-demand collection; thresholds, load limits, EXPLAIN dialect, and listeners |
## Entity beans
| Guide | Description |
|-------|-------------|
| [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; field visibility and accessor guidance; minimal boilerplate |
| [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` |
| [`@DbJson` mapping support (built-in vs Jackson)](dbjson-mapping-support.md) | Which `@DbJson` / `@DbJsonB` property types are handled by the built-in avaje-json-core support versus which require `ebean-jackson-mapper` (Jackson `ObjectMapper`); supported `String`/`List`/`Set`/`Map` matrix; enum-key and `@DbArray` notes |
| [Derived / formula properties (`@Formula`, `@Formula2`)](derived-formula-properties.md) | Read-only computed properties: physical-SQL `@Formula` (with `${ta}` and hand-written joins) versus logical path-based `@Formula2` (auto-resolved joins); use in `select`/`where`/`orderBy`; default inclusion and the `@Transient` opt-out |
## Querying
| Guide | Description |
|-------|-------------|
| [Write Ebean queries with query beans](writing-ebean-query-beans.md) | Step-by-step guidance for AI agents to write type-safe Ebean queries; choose the right terminal method; tune `select()` / `fetch()` / `fetchQuery()`; and project to DTOs when entity beans are not the right output |
| [Immutable bean cache for read-only references](immutable-bean-cache.md) | Use `ImmutableBeanCache` and `ImmutableBeanCaches.loading(...)` to resolve assoc-one references in read-only/unmodifiable queries, including secondary `fetchQuery`/`fetchLazy` loads |
| [Using `RawSql` with Ebean](using-rawsql-with-ebean.md) | Choose between `RawSqlBuilder.parse()`, `unparsed()`, and `withPlaceholders()`; the `${where}`/`${andWhere}`/`${having}`/`${andHaving}` placeholder reference for CTEs, window functions, and subqueries; column mapping; and using `RawSql` with query beans |
## Persisting & transactions
| Guide | Description |
|-------|-------------|
| [Persisting and transactions with Ebean](persisting-and-transactions-with-ebean.md) | Step-by-step guidance for AI agents to choose `insert` / `save` / `update` / `delete`; inspect cascades; select the right transaction boundary; and use batch or bulk update for large write sets |
## Testing
| Guide | Description |
|-------|-------------|
| [Testing with TestEntityBuilder](testing-with-testentitybuilder.md) | Rapidly create test entity instances with auto-populated random values; manage relationships and cascades; customize value generation for domain-specific testing needs |
## Database migrations
| Guide | Description |
|-------|-------------|
| [DB migration generation](add-ebean-db-migration-generation.md) | Add `GenerateDbMigration.java` to generate schema diff migrations offline; configure the migration runner; understand `.sql` and `.model.xml` output files; workflow for pending drops |
## Connection Pooling & DataSource Configuration
The [ebean-datasource](https://github.com/ebean-orm/ebean-datasource) project provides
comprehensive guides on connection pool configuration and best practices. These are particularly
useful for production deployments, especially in Kubernetes or AWS environments:
| Guide | Description |
|-------|-------------|
| [Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md) | Step-by-step guide for basic, read-only, Kubernetes, and AWS Lambda datasource configurations |
| [AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md) | Setting up dual DataSources with Aurora read and write endpoints for Ebean integration |
| [Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md) | Understanding connection heartbeat strategies and why `Connection.isValid()` is the recommended approach |
---
## Helping AI agents find these guides
### Agent Skills (recommended)
The **ebean-orm skill** packages these guides into a structured format that AI agents
load on-demand. See the [ebean-orm/skills](https://github.com/ebean-orm/skills) repo
for installation instructions. Quick start:
```bash
git clone git@github.com:ebean-orm/skills.git ~/.agents/ebean-skills
mkdir -p ~/.agents/skills
ln -sf ~/.agents/ebean-skills/ebean-orm ~/.agents/skills/ebean-orm
```
This works with [pi](https://github.com/mariozechner/pi-coding-agent),
[Claude Code](https://docs.anthropic.com/en/docs/claude-code), and any harness
that supports the [Agent Skills standard](https://agentskills.io/specification).
### Other approaches
AI coding agents can only follow these guides if they know they exist. Below are
copy-paste snippets for the most common AI tooling configurations. Add whichever
applies to your project.
### llms.txt (fastest bootstrap for any agent)
`https://ebean.io/llms.txt` provides a concise Ebean capability reference and links
to all guides in a format optimised for LLMs. Agents that check `llms.txt` before
starting work will find everything they need without requiring project-level
configuration. This is worth knowing if you are an AI agent reading this file.
### Project `README.md` (universal — works with all AI tools)
Add a section near the top of your project README:
```markdown
## AI Agent Instructions
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
Ebean tasks (Maven setup, database configuration, query writing, transactions,
test containers, DB migrations, Lombok) are at:
**https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/**
AI agents should fetch and follow the relevant guide before performing Ebean-related
tasks.
```
### Why README is important
Many AI agents (Copilot, Claude, Cursor, Aider) read the project `README.md`
automatically as part of understanding the codebase. A visible link in README is the
most universally effective way to ensure any agent working on your project will
discover the guides without requiring tool-specific configuration files.
------
### GitHub Copilot — `.github/copilot-instructions.md`
```markdown
## Ebean ORM
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
Key guides (fetch and follow these when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Immutable bean cache for read-only references: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/immutable-bean-cache.md
- Ebean OpenTelemetry tracing: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-opentelemetry.md
- Query metrics and naming: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-metrics.md
- Query plan capture: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/ebean-query-plan-capture.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
- Lombok with entity beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/lombok-with-ebean-entity-beans.md
```
### Claude Code — `CLAUDE.md`
Same content as above — Claude Code reads `CLAUDE.md` at the project root.
### AGENTS.md — OpenAI Codex / GitHub Copilot coding agent
Place an `AGENTS.md` at your repo root:
```markdown
## Ebean ORM
This project uses [Ebean ORM](https://ebean.io). Step-by-step guides for common
tasks are at: https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
Key guides (fetch and follow these when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Migrate to `Database.builder()`: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/migrating-to-database-builder.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
- Entity bean creation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/entity-bean-creation.md
```
### Cursor — `.cursor/rules/ebean.mdc`
```markdown
---
description: Ebean ORM task guidance
globs: ["**/*.java", "**/pom.xml"]
alwaysApply: false
---
## Ebean ORM
This project uses Ebean ORM. Before performing any Ebean-related task, fetch and
follow the relevant step-by-step guide from:
https://github.com/ebean-orm/ebean/tree/HEAD/docs/guides/
```
@@ -1,400 +0,0 @@
# Guide: Add Ebean Database Migration Generation to an Existing Maven Project
## Purpose
This guide provides step-by-step instructions for adding Ebean DB migration generation
to an existing Maven project that already uses Ebean ORM. Ebean generates migrations by
performing a diff of the current entity model against the previously recorded model state,
producing platform-specific DDL SQL scripts.
These instructions are designed for AI agents and developers to follow precisely.
---
## Prerequisites
- An existing Maven project with Ebean ORM configured (entity beans present)
- `ebean-test` is already a test-scoped dependency (from POM setup guide)
- The project targets PostgreSQL (adjust `Platform.POSTGRES` for other databases)
---
## Step 1 — Verify migration dependencies
### Generation tooling (`ebean-ddl-generator`)
`ebean-test` (already present as a test dependency) transitively includes
`ebean-ddl-generator`, which provides the `DbMigration` class. No additional dependency
is required for generation.
### Runtime migration runner (`ebean-migration`)
`ebean-migration` is the library that runs migrations on application startup.
It is typically included **transitively** via `io.ebean:ebean-postgres` (or the
equivalent platform dependency). Verify it is on the classpath by running:
```bash
mvn dependency:tree | grep ebean-migration
```
If it is **not** present transitively, add it explicitly as a compile-scope dependency:
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean.version}</version>
</dependency>
```
---
## Step 2 — Create `GenerateDbMigration.java`
Create the following class in `src/test/java/main/`. This `main` method is run manually
by a developer (or AI agent) whenever entity beans change and a new migration is needed.
```java
package main;
import io.ebean.annotation.Platform;
import io.ebean.dbmigration.DbMigration;
import java.io.IOException;
/**
* Generate the next database migration based on a diff of the entity model.
* Run this main method after making entity bean changes to produce the migration SQL.
*/
public class GenerateDbMigration {
public static void main(String[] args) throws IOException {
DbMigration migration = DbMigration.create();
migration.setPlatform(Platform.POSTGRES);
migration.setVersion("1.1"); // set to the next migration version
migration.setName("add-customer"); // short description of the change
migration.generateMigration();
}
}
```
### Version naming convention
Ebean supports two common version formats — choose one and apply it consistently:
| Format | Example | Notes |
|--------|---------|-------|
| **Date-based** | `20240820` | `YYYYMMDD`; used when changes are tied to dates; easily sortable |
| **Semantic** | `1.1`, `1.2`, `2.0` | Traditional versioning; useful for release-based workflows |
The version controls execution order — Ebean runs migrations in ascending version order.
### Name convention
The `name` should be a short, lowercase, hyphenated description of the change:
- `add-customer-email`
- `rename-machine-type`
- `drop-unused-columns`
---
## Step 3 — Configure the output path (if needed)
By default, migration files are written to `src/main/resources/dbmigration/` relative
to the **current working directory** when `generateMigration()` is called. This is
usually the module root, which is correct for single-module projects.
For **multi-module projects** where `GenerateDbMigration` is in a submodule but the
resources directory is at a different relative path, specify it explicitly:
```java
// Relative path from the working directory (project root) to the module's resources
migration.setPathToResources("my-module/src/main/resources");
```
---
## Step 4 — Run `GenerateDbMigration` to produce the first migration
Run the `main` method via the IDE or Maven:
```bash
# Run via Maven exec plugin (or use IDE run configuration)
mvn test-compile exec:java \
-Dexec.mainClass="main.GenerateDbMigration" \
-Dexec.classpathScope="test" \
-pl <your-module>
```
Ebean migration generation runs in **offline mode** — no database connection is required.
### Expected output files
After running, two files are created per migration in `src/main/resources/dbmigration/`:
```
src/main/resources/dbmigration/
1.1__add-customer.sql ← DDL SQL to apply (commit this)
model/
1.1__add-customer.model.xml ← logical model diff XML (commit this)
```
Both files must be committed to source control. The `.model.xml` file records the
logical state of the diff and is used by subsequent migration generations to determine
what has changed.
If **no entity beans have changed** since the last migration, the command outputs:
```
DbMigration - no changes detected - no migration written
```
---
## Step 5 — Enable the migration runner
Configure Ebean to run pending migrations automatically on application startup.
### Preferred approach — programmatic via `DatabaseBuilder`
Set `runMigration(true)` directly on the `DatabaseBuilder` when constructing
the `Database` bean. This is the preferred approach as it is explicit, co-located with
the database configuration, and does not rely on external property files.
In the `@Factory` class that builds the `Database` bean (see the database configuration
guide), add `.runMigration(true)` to the builder chain:
```java
@Bean
Database database(ConfigWrapper config) {
var dataSource = DataSourceBuilder.create()
.url(config.getDatabaseUrl())
.username(config.getDatabaseUser())
.password(config.getDatabasePassword())
// ... other datasource settings ...
;
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.runMigration(true) // run pending migrations on startup
.build();
}
```
If migrations should only run in certain environments (e.g., not in production, or
only when a config flag is set), make it conditional:
```java
.runMigration(config.isRunMigrations()) // driven by config value
```
### Alternative — via application properties
If programmatic configuration is not available or not preferred, set the property
in `src/main/resources/application.properties`:
```properties
ebean.migration.run=true
```
Or in `src/main/resources/application.yaml`:
```yaml
ebean:
migration:
run: true
```
For a **named database** (i.e., `Database.builder().name("mydb")`), use the database
name in the property key:
```properties
ebean.mydb.migration.run=true
```
### What the runner does at startup
When migration running is enabled, Ebean will on each application start:
1. Look at the migrations in `src/main/resources/dbmigration/`
2. Compare against the `db_migration` table (created automatically on first run)
3. Apply any migrations that have not yet been executed, in version order
4. Record each successfully applied migration in `db_migration`
---
## Step 6 — Commit the migration files
Add both generated files to source control:
```bash
git add src/main/resources/dbmigration/1.1__add-customer.sql
git add src/main/resources/dbmigration/model/1.1__add-customer.model.xml
git commit -m "Add db migration 1.1: add-customer"
```
---
## Ongoing workflow — generating subsequent migrations
For each future set of entity bean changes:
1. Make changes to the entity bean classes
2. Update `GenerateDbMigration.java` with the **new version** and **new name**:
```java
migration.setVersion("1.2");
migration.setName("add-address-table");
```
3. Run the `main` method — a new `.sql` and `.model.xml` pair is written
4. Review the generated `.sql` to confirm it reflects the intended changes
5. Commit both files
### Protecting hand-edited and non-versioned migrations across regeneration
`GenerateDbMigration` regenerates the apply SQL and model XML from the **current
entity model**. It can therefore overwrite content you did not change in the
entity beans, including:
- **hand-edited DDL** in a generated versioned `.sql` file, and
- **repeatable** (`R__*.sql`) scripts that the generator also derives from the
model (e.g. view definitions in `extra-ddl.xml`, built-in partitioning helpers).
**Init scripts (`I__*.sql`) are write-once.** If an init script already exists on
disk the generator **does not** rewrite it, so hand-tuned init DDL (partition
functions, `UNLOGGED` tables, triggers, seed data) is preserved across
regeneration. The trade-off: to pick up an upstream change to a built-in init
script (e.g. the partition helper) you must **delete the file first**, then
regenerate. Repeatable scripts are always regenerated.
To avoid losing manual work:
- Prefer an **init** (`I__`) script for hand-maintained DDL the entity model
cannot express — it is isolated and now protected from regeneration.
- For **versioned** `.sql` and **repeatable** `R__` scripts that the generator
produces, review the diff after **every** regeneration and **restore** any
clobbered hand-tuning (e.g. `git checkout dbmigration/...`) before committing.
- If your build maintains a migration index file (e.g. `idx_*.migrations`),
re-check that the new migration is listed and filenames match after renaming a
generated file.
> **Run the generator from the module directory.** The output path set via
> `setPathToResources(...)` is resolved relative to the **working directory**.
> Run `GenerateDbMigration` with the working directory set to the module that owns
> `src/main/resources` (e.g. `cd server` first). Note that `mvn exec:java` does
> **not** honour a configured `workingDirectory`, so set the cwd yourself.
---
## Understanding the output files
### Apply SQL (`.sql`)
The apply SQL file contains the DDL that will be executed against the database:
```sql
-- apply changes
alter table customer add column email varchar(255);
```
### Model XML (`.model.xml`)
The model XML records the logical diff in a database-agnostic format. Ebean uses
this file on the next generation run to determine what has already been captured.
It is not executed against the database.
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<migration xmlns="http://ebean-orm.github.io/xml/ns/dbmigration">
<changeSet type="apply">
<addColumn tableName="customer">
<column name="email" type="varchar(255)"/>
</addColumn>
</changeSet>
</migration>
```
---
## Optional configurations
### Multiple database platforms
To generate migrations for multiple platforms simultaneously, use `addPlatform()`
instead of `setPlatform()`:
```java
migration.addPlatform(Platform.POSTGRES);
migration.addPlatform(Platform.SQLSERVER17);
migration.addPlatform(Platform.MYSQL);
```
Each platform gets its own subdirectory under `dbmigration/`.
### Include index
When enabled the migration generation also generates a file that contains
all the migrations and their associated hashes. This is a performance
optimisation (that will become the default) and means that the migration
runner just needs to read the one resource and has the pre-computed hash
values (so does not need to read each migration resource and compute the
hash for each of those at runtime).
```java
migration.setIncludeIndex(true);
```
### Strict mode
Strict mode (on by default) errors if there are any pending drops not yet applied.
Set to `false` to allow generation to proceed regardless:
```java
migration.setStrictMode(false);
```
### Applying pending drops
Destructive changes (drop column, drop table) are **not** included in the apply
SQL by default — they are recorded as `pendingDrops` in the model XML. This allows
the application to be deployed without immediately dropping columns (important for
rolling deployments).
The migration runner logs a message when pending drops exist:
```
INFO DbMigration - Pending un-applied drops in versions [1.1]
```
When ready to apply the drops, set `setGeneratePendingDrop` to the version that
contains the pending drops:
```java
migration.setVersion("1.3");
migration.setName("drop-pending-from-1.1");
migration.setGeneratePendingDrop("1.1"); // apply drops recorded in version 1.1
migration.generateMigration();
```
### Custom dbSchema
If the project uses a named Postgres schema (set via `ebean.dbSchema` in
`application.properties`), no additional configuration is needed in
`GenerateDbMigration` — Ebean picks up the schema from the application config
automatically when running in offline mode.
```properties
# application.properties
ebean.dbSchema=myschema
```
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| `no changes detected - no migration written` | Entity beans unchanged since last migration | Make entity bean changes first, then re-run |
| `DbMigration - Pending un-applied drops` | A previous migration has drops not yet applied | Either suppress with `setStrictMode(false)` or apply drops with `setGeneratePendingDrop(...)` |
| Generated SQL is empty or wrong | Wrong working directory path | Set `setPathToResources(...)` to the correct module-relative path |
| `ClassNotFoundException` for entity classes | Test classpath not including main classes | Ensure `exec.classpathScope=test` or run via IDE with test classpath |
| Migrations not running on startup | Property key wrong or `ebean-migration` missing | Verify `ebean[.name].migration.run=true` and that `ebean-migration` is on the classpath |
-158
View File
@@ -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 5075% of
`maxConnections`.
---
## Step 3 — Inject configuration via a constructor or config helper (recommended)
Rather than calling `Config.get(...)` inline, inject a typed config helper or the
Avaje `Configuration` bean if one is available. This makes the factory testable and
keeps the wiring explicit. For example:
```java
@Bean
Database database(Configuration config) {
String url = config.get("db_url");
String user = config.get("db_user");
String pass = config.get("db_pass");
int min = config.getInt("db_master_min_connections", 1);
int init = config.getInt("db_master_initial_connections", 10);
int max = config.getInt("db_master_max_connections", 200);
var dataSource = DataSourceBuilder.create()
.url(url)
.username(user)
.password(pass)
.driver("org.postgresql.Driver")
.schema("myschema")
.applicationName("my-app")
.minConnections(min)
.initialConnections(init)
.maxConnections(max);
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.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`.
-294
View File
@@ -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 Springs `@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.
-116
View File
@@ -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.
-164
View File
@@ -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).
-262
View File
@@ -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.
-242
View File
@@ -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.
-797
View File
@@ -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/`
-136
View File
@@ -1,136 +0,0 @@
# Immutable bean cache for read-only references
This guide shows how to use `ImmutableBeanCache` for read-mostly assoc-one
references (for example `Label` references reused across many entities).
Use this when you want:
- fewer lazy-load SQL calls for assoc-one references via caching
- reusable fetch-group-based loading for cache misses
---
## Step 1 - Build an immutable cache (typical via builder)
```java
FetchGroup<Label> fetchGroup = FetchGroup.of(Label.class)
.select("version")
.fetch("labelTexts", "locale, localeText")
.build();
ImmutableBeanCache<Label> labelCache = ImmutableBeanCaches.builder(Label.class)
.loading(database, fetchGroup)
.maxSize(10_000)
.maxIdleSeconds(300)
.maxSecondsToLive(6_000)
.build();
```
`loading(...)` uses the query shape:
- `select(fetchGroup)`
- `setUnmodifiable(true)`
- `where().idIn(ids)`
- `findMap()`
Alternative domain example:
```java
FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
.select("name,version")
.fetch("billingAddress", "line1,city")
.fetch("shippingAddress", "line1,city")
.build();
ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
.loading(database, customerGroup)
.build();
```
---
## Step 2 - Attach cache to the root query
```java
AttributeDescriptor one = DB.find(AttributeDescriptor.class)
.setId(id)
.setUnmodifiable(true)
.using(labelCache)
.findOne();
```
`using(...)` is on the root query. Ebean will use this cache for matching
bean types when resolving references.
---
## Use loading helper for simple memoization
If you don't need policy controls, use the shorthand helper:
```java
ImmutableBeanCache<Label> labelCache =
ImmutableBeanCaches.loading(Label.class, database, FetchGroup.of(Label.class, "version"));
```
With `ebean-core` on the classpath, builder policy settings are backed by core
cache implementation (including periodic trim / eviction).
---
## Unmodifiable vs mutable query behavior
### Unmodifiable query path
`setUnmodifiable(true)` disables lazy loading. If you need association content
in cached beans make sure that is included in the fetch group.
```java
FetchGroup<Label> withTexts = FetchGroup.of(Label.class)
.select("version")
.fetch("labelTexts", "locale, localeText")
.build();
```
### Mutable query path
On a mutable query (no `setUnmodifiable(true)`), references populated from the
immutable cache are still mutable beans in that object graph. Additional
unloaded properties can still lazy load as normal.
Typical pattern:
1. cache serves already-loaded reference properties (for example `version`)
2. later access to unloaded properties (for example `labelTexts`) triggers
normal lazy loading
---
## Understand secondary query behavior (`+query`, `+lazy`)
When root queries execute secondary loads (`fetchQuery(...)` or `fetchLazy(...)`),
the immutable caches configured on the root query are propagated to those
secondary queries.
That means assoc-one references resolved in secondary query paths can still hit
the immutable cache.
---
## Operational note (TTL / max size)
Use `ImmutableBeanCaches.builder(...)` when you need explicit TTL/max-size
policy. `ImmutableBeanCaches.loading(...)` remains the simple helper for
loader-based memoization.
---
## Testing checklist
1. Hit / partial hit / miss behavior for `getAll(ids)`
2. Unmodifiable path: no lazy SQL when reading loaded reference properties
3. Mutable path: additional unloaded properties can still lazy load
4. Secondary `fetchQuery` and `fetchLazy` paths inherit immutable caches
5. If needed associations are in fetch group, assert no extra SQL for those
accesses
@@ -1,206 +0,0 @@
# Guide: Using Lombok with Ebean Entity Beans
## Purpose
This guide explains which Lombok annotations are safe and recommended for Ebean
entity beans, which ones to avoid, and why. It is written as prescriptive instructions
for AI agents and developers.
---
## The Core Rule
> **Do NOT use `@Data` on Ebean entity beans.**
Use `@Getter` + `@Setter` instead, with the optional `@Accessors(chain = true)` for
a fluent setter style.
---
## Why `@Data` is Incompatible with Ebean
`@Data` is a convenience annotation that is equivalent to applying `@Getter`,
`@Setter`, `@RequiredArgsConstructor`, `@ToString`, and `@EqualsAndHashCode` together.
Three of those are problematic for Ebean entity beans:
### 1. `@EqualsAndHashCode` (included in `@Data`) — breaks entity identity
`@Data` generates `hashCode()` and `equals()` based on all non-static, non-transient
fields. Ebean entity beans have identity semantics — two references to the same database
row should be considered equal based on their `@Id` value, not field-by-field comparison.
Problems caused:
- Inconsistent `hashCode` before and after persist (the `@Id` field is `0` on a new
entity, then changes after insert — violating the `hashCode` contract for collections)
- Entities placed in a `Set` or `HashMap` before saving will be unfindable after saving
- Ebean's internal identity map and dirty checking can be confused
### 2. `@ToString` (included in `@Data`) — triggers unexpected lazy loading
`@Data` generates a `toString()` that accesses **all** fields, including
`@OneToMany` and `@ManyToOne` associations. Accessing an unloaded lazy association
outside of a transaction triggers a `LazyInitialisationException` or fires an unexpected
SQL query, which can:
- Cause subtle bugs in logging statements
- Trigger N+1 queries in test output or debug logging
- Fail with an exception if no active transaction exists
### 3. `@RequiredArgsConstructor` (included in `@Data`) — unnecessary for Ebean
Ebean does not require a default constructor — it can construct entity instances without
one. `@RequiredArgsConstructor` therefore adds nothing useful to entity beans.
---
## Recommended Annotation Set
Use exactly these three Lombok annotations on every Ebean entity bean:
```java
@Entity
@Getter
@Setter
@Accessors(chain = true)
@Table(name = "my_table")
public class MyEntity {
// ...
}
```
| Annotation | Purpose |
|---|---|
| `@Getter` | Generates `getFoo()` / `isFoo()` accessor methods |
| `@Setter` | Generates `setFoo(value)` mutator methods; Ebean enhancement intercepts these for dirty tracking |
| `@Accessors(chain = true)` | Makes setters return `this`, enabling fluent/builder-style property setting |
---
## `@Accessors(chain = true)` — Fluent Setter Style
With `chain = true`, setters return `this` instead of `void`, allowing method chaining:
```java
// without chain = true (void setters)
CMachine machine = new CMachine();
machine.setMake("Toyota");
machine.setModel("Hilux");
machine.setStatus("active");
// with @Accessors(chain = true)
CMachine machine = new CMachine()
.setMake("Toyota")
.setModel("Hilux")
.setStatus("active");
```
This is particularly useful when building test data:
```java
CMachine machine = new CMachine()
.setGid(UUID.randomUUID())
.setMachineType("HV")
.setStatus("active")
.setMake("Komatsu")
.setModel("PC200");
database.save(machine);
```
Ebean's bytecode enhancement is fully compatible with chained setters — the
enhancement intercepts each `setFoo()` call to record which fields have been modified
(dirty checking), regardless of whether the setter returns `void` or `this`.
---
## `@Accessors(fluent = true)` — also compatible
`@Accessors(fluent = true)` removes the `get`/`set`/`is` prefix, generating `name()`
(getter) and `name(value)` (setter) instead of `getName()` and `setName(value)`.
Ebean does **not** require JavaBeans naming conventions — it can work with any accessor
method style, including fluent accessors with no prefix. `@Accessors(fluent = true)` is
therefore compatible with Ebean.
`@Accessors(chain = true)` is the more common choice in practice (it keeps the familiar
`get`/`set` prefix while adding method chaining), but `fluent = true` is a valid
alternative if that style is preferred consistently across the codebase.
---
## Full Entity Bean Example
```java
package com.example.repository.data;
import io.ebean.annotation.WhenCreated;
import io.ebean.annotation.WhenModified;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
@Entity
@Getter
@Setter
@Accessors(chain = true)
@Table(name = "machine")
public class CMachine {
@Id
private long id;
@Version
private int version;
@Column(nullable = false, unique = true)
private UUID gid;
@Column(nullable = false, length = 10)
private String machineType;
@Column(length = 200)
private String make;
@Column(length = 200)
private String model;
@WhenCreated
private Instant created;
@WhenModified
private Instant lastModified;
}
```
---
## Summary: Lombok Annotations and Ebean Compatibility
| Lombok Annotation | Compatible? | Notes |
|---|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `@Getter` | ✅ Safe | Use on every entity bean |
| `@Setter` | ✅ Safe | Use on every entity bean; enhancement intercepts these |
| `@Accessors(chain = true)` | ✅ Safe | Recommended for fluent construction style |
| `@ToString` | ❌ Avoid | Ebean does a better job and handles recursion |
| `@EqualsAndHashCode` | ❌ Avoid | Breaks entity identity and `@Id`-based equality |
| `@Data` | ❌ Avoid | Includes `@EqualsAndHashCode` and `@ToString` — both problematic |
| `@Value` | ❌ Avoid | Makes fields final — incompatible with Ebean's field-level bytecode enhancement |
| `@Accessors(fluent = true)` | ✅ Safe | Removes `get`/`set` prefix — Ebean does not require JavaBeans naming conventions and works with any accessor style |
| `@Builder` | ⚠️ Careful | Usable on non-entity helper/factory classes; on entity beans it requires a no-arg constructor alongside it and offers no advantage over `@Accessors(chain = true)` |
---
## Relationship with Ebean Bytecode Enhancement
Ebean's bytecode enhancement (applied by `ebean-maven-plugin` at build time) modifies
the `setXxx()` methods of entity beans to:
1. Mark the field as dirty (changed) so only modified fields are included in UPDATE statements
2. Support lazy loading of associations when a getter is called on an unloaded field
For this to work correctly, Ebean needs:
- Accessor methods for each persistent field (any naming style is fine — `getFoo()`, `foo()`, or no accessors at all; Ebean can also access fields directly)
- No override of `hashCode()` / `equals()` that would interfere with the identity map — which means **no `@Data` or `@EqualsAndHashCode`**
@@ -1,91 +0,0 @@
# Guide: Migrate JSON APIs from Jackson core to avaje-json-core
## Purpose
This guide covers the one-step cutover in Ebean from Jackson core JSON APIs to
avaje-json-core APIs.
Use this when upgrading code that references:
- `com.fasterxml.jackson.core.JsonParser`
- `com.fasterxml.jackson.core.JsonGenerator`
- `com.fasterxml.jackson.core.JsonFactory`
The replacement types are:
- `io.avaje.json.JsonReader`
- `io.avaje.json.JsonWriter`
- `io.avaje.json.stream.JsonStream`
---
## Breaking API changes
| Previous API | New API |
|---|---|
| `JsonParser` | `JsonReader` |
| `JsonGenerator` | `JsonWriter` |
| `JsonFactory` | `JsonStream` |
| `DatabaseBuilder.jsonFactory(...)` | `DatabaseBuilder.jsonStream(...)` |
| `DatabaseConfig.getJsonFactory()/setJsonFactory(...)` | `DatabaseConfig.getJsonStream()/setJsonStream(...)` |
---
## Typical migration rewrites
### Parser and generator signatures
```java
// before
void read(JsonParser parser)
void write(JsonGenerator generator)
// after
void read(JsonReader parser)
void write(JsonWriter generator)
```
### Database configuration
```java
// before
Database.builder().jsonFactory(factory)
// after
Database.builder().jsonStream(stream)
```
### JSON utility calls
`EJson` and `JsonContext` APIs now operate on `JsonReader` and `JsonWriter` types.
If your code was calling those APIs with Jackson core types, switch to avaje types.
---
## Dependency and module notes
- `ebean-core` no longer requires a direct `jackson-core` dependency for JSON
parsing/writing.
- `jackson-databind` remains optional for `ObjectMapper` compatibility paths.
- `ebean-jackson-mapper` remains the compatibility bridge module for mapper-based
integrations.
---
## Behavior notes to verify during upgrade
1. Parser token handling is now based on avaje `JsonReader.Token`.
2. Scalar JSON reads (for example booleans, date-time, array scalar types) should
be validated in your tests if you previously depended on Jackson token quirks.
3. If your integration uses transient assoc-many JSON mapping with ObjectMapper,
keep ObjectMapper wiring enabled.
---
## Validation checklist
1. Compile all modules that implement or consume `io.ebean.text.json` APIs.
2. Run module tests that cover JSON scalar conversion and bean JSON round-trips.
3. Confirm no remaining `com.fasterxml.jackson.core.*` imports in migrated code.
4. Keep `ObjectMapper` compatibility tests if your project depends on mapper paths.
@@ -1,242 +0,0 @@
# Guide: Migrate from `DatabaseConfig` / `DatabaseFactory` to `Database.builder()`
## Purpose
This guide shows how to migrate legacy programmatic database creation code from:
- `new DatabaseConfig()`
- `DatabaseFactory.create(...)`
- old `setXxx(...)` builder-style configuration methods
…to the preferred builder-based style using:
- `Database.builder()`
- fluent `DatabaseBuilder` methods such as `name(...)`, `register(...)`, and `defaultDatabase(...)`
- `DatabaseBuilder.build()`
Use this guide when upgrading older Ebean setup code or when building an automated/semi-automated migration.
---
## Preferred pattern
Prefer code shaped like this:
```java
Database database = Database.builder()
.name("db")
.loadFromProperties()
.dataSourceBuilder(dataSource)
.register(true)
.defaultDatabase(true)
.build();
```
The important points are:
1. Start with `Database.builder()`
2. Configure via `DatabaseBuilder`
3. Finish with `.build()`
---
## Step 1 — Replace `new DatabaseConfig()` with `Database.builder()`
### Before
```java
DatabaseConfig config = new DatabaseConfig();
config.setName("db");
config.loadFromProperties();
```
### After
```java
DatabaseBuilder config = Database.builder()
.name("db")
.loadFromProperties();
```
### Notes
- Prefer the `DatabaseBuilder` type for local variables and parameters when possible.
- If existing code only uses standard builder methods, this change is usually mechanical.
- If existing code later reads configuration back, use `config.settings()`.
---
## Step 2 — Replace `DatabaseFactory.create(config)` with `config.build()`
### Before
```java
DatabaseConfig config = new DatabaseConfig();
config.setName("db");
config.loadFromProperties();
Database database = DatabaseFactory.create(config);
```
### After
```java
DatabaseBuilder config = Database.builder()
.name("db")
.loadFromProperties();
Database database = config.build();
```
### Short form
```java
Database database = Database.builder()
.name("db")
.loadFromProperties()
.build();
```
---
## Step 3 — Replace `DatabaseFactory.create("name")`
### Before
```java
Database database = DatabaseFactory.create("other");
```
### After
```java
Database database = Database.builder()
.name("other")
.loadFromProperties()
.build();
```
### Important
For **named databases**, set `.name("...")` before `.loadFromProperties()` so the named configuration is loaded.
---
## Step 4 — Replace legacy `setXxx(...)` methods with fluent builder methods
`DatabaseBuilder` already exposes preferred fluent names for most configuration methods.
Use those names when migrating older setup code.
| Legacy call | Preferred call |
|---|---|
| `setName("db")` | `name("db")` |
| `setRegister(false)` | `register(false)` |
| `setDefaultServer(false)` | `defaultDatabase(false)` |
| `setContainerConfig(cfg)` | `containerConfig(cfg)` |
| `setDbSchema("app")` | `dbSchema("app")` |
| `setDataSourceConfig(ds)` | `dataSourceBuilder(ds)` |
| `setReadOnlyDataSourceConfig(ro)` | `readOnlyDataSourceBuilder(ro)` |
| `setRunMigration(true)` | `runMigration(true)` |
| `setDisableClasspathSearch(true)` | `disableClasspathSearch(true)` |
| `setPersistBatch(batch)` | `persistBatch(batch)` |
### Full example
#### Before
```java
DatabaseConfig config = new DatabaseConfig();
config.setName("db");
config.setRegister(false);
config.setDefaultServer(false);
config.setDataSourceConfig(dataSource);
Database database = DatabaseFactory.create(config);
```
#### After
```java
Database database = Database.builder()
.name("db")
.register(false)
.defaultDatabase(false)
.dataSourceBuilder(dataSource)
.build();
```
---
## Step 5 — Verify semantics after migration
The migration should preserve behavior, but verify these points:
- `register(true)` is still the default
- `defaultDatabase(true)` is still the default
- call `loadFromProperties()` if the old code loaded configuration from properties
- for named databases, set the name before loading properties
- explicit entity registration via `addClass(...)` / `addAll(...)` is unchanged
- custom datasource wiring via `dataSourceBuilder(...)` and `readOnlyDataSourceBuilder(...)` is unchanged
---
## Manual-review cases
These cases are **not** simple search-and-replace migrations and should be reviewed manually:
### `DatabaseFactory.createWithContextClassLoader(...)`
There is no direct builder shorthand for this today. Keep this as-is for now and migrate the surrounding builder configuration first.
### `DatabaseFactory.initialiseContainer(...)`
This is a container lifecycle concern, not a normal database-builder call. Keep it as-is unless you are intentionally moving the `ContainerConfig` onto the first builder via `containerConfig(...)`.
### `DatabaseFactory.shutdown()`
This is also a lifecycle concern rather than normal builder setup. Leave it alone unless you are making a deliberate lifecycle change.
### Variables or method signatures typed as `DatabaseConfig`
If the code only uses standard builder operations, switch the type to `DatabaseBuilder`.
If the code depends on implementation-specific `DatabaseConfig` methods, review it manually.
### Code that needs read access to builder settings
Use:
```java
DatabaseBuilder builder = Database.builder();
DatabaseBuilder.Settings settings = builder.settings();
```
rather than relying on the concrete `DatabaseConfig` type only to read getters.
---
## Automation notes for AI agents and bulk refactors
This migration is a good candidate for semi-automated upgrading.
### Safe mechanical rewrites
These are usually safe to rewrite automatically:
- `new DatabaseConfig()``Database.builder()`
- `DatabaseFactory.create(builder)``builder.build()`
- `DatabaseFactory.create("name")``Database.builder().name("name").loadFromProperties().build()`
- legacy `setXxx(...)` calls → preferred fluent builder methods
### Flag for manual review
Automatically flag, but do not blindly rewrite:
- `DatabaseFactory.createWithContextClassLoader(...)`
- `DatabaseFactory.initialiseContainer(...)`
- `DatabaseFactory.shutdown()`
- parameters, fields, or return types declared as `DatabaseConfig`
- any use that clearly depends on `DatabaseConfig` implementation details rather than `DatabaseBuilder`
---
## Related guides
- [Database configuration](add-ebean-postgres-database-config.md) — preferred modern setup style using `Database.builder()`
- [Guide index](README.md) — full list of Ebean setup and migration guides
@@ -1,447 +0,0 @@
# Guide: Persist Changes and Manage Transactions with Ebean
## Purpose
This guide gives step-by-step instructions for AI agents and developers to save,
update, delete, and batch changes with Ebean while choosing the correct
transaction boundary.
Use this guide when you need to:
- create a new entity row
- update one or more existing rows
- delete rows safely
- decide between implicit transactions, `@Transactional`, and explicit
transactions
- batch or bulk-write many rows efficiently
The default recommendation is:
1. Choose the correct persistence operation first
2. Use implicit transactions for a single isolated write
3. Use `@Transactional` for multi-step application workflows
4. Use explicit transactions only when you need explicit control
5. Use bulk update or batching for large write sets
---
## Prerequisites
- The project already uses Ebean ORM
- Entity beans and database configuration already exist
- You know which `Database` is being used (`DB.getDefault()` or a named database)
If the project is not yet configured, first follow:
- [`add-ebean-postgres-database-config.md`](add-ebean-postgres-database-config.md)
- [`entity-bean-creation.md`](entity-bean-creation.md)
---
## Step 1 - Choose the correct persistence operation before editing code
Do not start with `database.save(...)` by habit. First decide what kind of change the
caller is making.
| Need | Preferred API | Use when |
|------|---------------|----------|
| Insert a bean that is definitely new | `database.insert(bean)` | New-create flow, seed data, fixture setup |
| Save a bean that may be new or existing | `database.save(bean)` | Common default when bean state determines insert vs update |
| Update a bean that is definitely existing | `database.update(bean)` | Existing row should be updated only |
| Delete one bean | `database.delete(bean)` | Remove a loaded entity bean |
| Update many rows without loading beans | `database.update(...)` or `query.asUpdate()` | Set-based write, not per-row business logic |
| Delete many rows without loading beans | bulk update/delete API or `database.sqlUpdate(...)` | Set-based deletion |
### Agent rule
Choose the operation that matches intent:
- known new row -> `insert`
- known existing row -> `update`
- uncertain/new-or-existing -> `save`
- many rows -> bulk update/delete, not a loop of individual saves
### Style note
Use a `Database` instance for all persistence operations: `database.save(bean)`,
`database.insert(bean)`, `database.update(bean)`, `database.delete(bean)`.
Inject the `Database` bean or obtain it via `DB.getDefault()`. Avoid using the
static `DB.*` convenience methods.
---
## Step 2 - Persist single-bean changes with the correct API
### Example - insert a known new bean
```java
Customer customer = new Customer();
customer.setName("Rob");
customer.setEmail("rob@example.com");
database.insert(customer);
```
### Example - update an existing bean
```java
Customer customer = new QCustomer()
.id.equalTo(customerId)
.findOne();
customer.setStatus(Customer.Status.ACTIVE);
database.update(customer);
```
### When to prefer `insert()` over `save()`
Use `insert()` when the code is creating a brand new row and should fail if the
operation does not behave like an insert.
### When to prefer `update()` over `save()`
Use `update()` when the bean is definitely existing and the method should not
silently behave like an insert.
---
## Step 3 - Check cascade mappings before assuming related beans will persist or delete
Ebean follows cascade rules defined on mapping annotations such as
`@OneToMany`, `@OneToOne`, `@ManyToOne`, and `@ManyToMany`.
The default is **no cascade**.
### Example
```java
@Entity
public class Order {
@ManyToOne
private Customer customer; // no cascade by default
@OneToMany(cascade = CascadeType.ALL)
private List<OrderDetail> details; // save + delete cascade
}
```
```java
database.save(order);
```
With the mapping above:
- `details` are cascaded
- `customer` is **not** cascaded
### Agent rules for cascades
1. Inspect the mapping before writing save/delete logic
2. Do not assume `@ManyToOne` cascades
3. Avoid adding cascade to shared parent references unless ownership is truly
intended
4. If a relationship should not cascade, save/delete related beans explicitly
---
## Step 4 - Let Ebean use an implicit transaction for a single isolated write
If the method performs one isolated persistence operation, Ebean can manage the
transaction implicitly.
### Good fit for implicit transaction
```java
Customer customer = new QCustomer()
.id.equalTo(customerId)
.findOne();
customer.setStatus(Customer.Status.INACTIVE);
database.save(customer);
```
### Good fit
- one save
- one update
- one delete
- small helper method with a single write
### Poor fit
- multiple writes that must commit or roll back together
- query + save + save workflow
- any method where later failure must roll back earlier writes
### Important
Queries also use implicit transactions when needed. You generally do **not**
need to wrap ordinary read queries in an explicit transaction "just in case".
---
## Step 5 - Use `@Transactional` for multi-step service workflows
When multiple Ebean operations belong to one unit of work, use
`@Transactional`.
### Example - service method
```java
import io.ebean.annotation.Transactional;
@Transactional
public void shipOrder(long orderId) {
Order order = new QOrder()
.id.equalTo(orderId)
.findOne();
order.setStatus(Order.Status.SHIPPED);
database.save(order);
Shipment shipment = new Shipment(order, Instant.now());
database.insert(shipment);
}
```
All database work inside the method runs in one transaction and commits only if
the method completes successfully.
### Use `Transaction.current()` only when needed
If the method needs access to the current transaction itself:
```java
Transaction txn = Transaction.current();
```
Do this only for transaction-specific behavior such as comments, savepoints, or
other advanced control. Do not fetch the current transaction if the method does
not need it.
### Agent rules for `@Transactional`
1. Put it on application/service workflow methods, not everywhere by default
2. Keep the transaction focused on database work
3. Avoid remote HTTP calls, message publishing, or long-running CPU work inside
the transaction if those can be moved outside
### Named database note
If the method uses a non-default database, obtain that `Database` instance via
`DB.byName("...")` and consistently use that database for both queries and
writes.
---
## Step 6 - Use `beginTransaction()` when you need explicit control
Use an explicit transaction when you need manual `commit()`, batching, explicit
flush, savepoints, or other low-level transaction control.
### Example - explicit transaction with try-with-resources
```java
try (Transaction txn = database.beginTransaction()) {
Order order = new QOrder()
.id.equalTo(orderId)
.findOne();
order.cancel();
database.save(order);
AuditLog auditLog = new AuditLog("order-cancelled", orderId);
database.insert(auditLog);
txn.commit();
}
```
If `commit()` is not reached, closing the transaction rolls it back.
### Useful explicit controls
- `txn.commit()` - commit current work
- `txn.setRollbackOnly()` - force rollback-only behavior
- `txn.flush()` - push batched statements to the database now
### Agent rule
Prefer `@Transactional` unless explicit transaction control is actually needed.
Do not use `beginTransaction()` only because it feels "safer".
---
## Step 7 - Use `createTransaction()` only for non-thread-local transaction handling
`createTransaction()` creates a transaction that is **not** placed into the
thread-local scope. This is a specialized tool.
Use it when:
- the transaction will be passed explicitly
- you need more than one transaction in the same thread
- you are coordinating work across threads or lower-level APIs
### Example - explicit transaction passed to query and save
```java
Database database = DB.getDefault();
try (Transaction txn = database.createTransaction()) {
Customer customer = new QCustomer(txn)
.email.equalTo(email)
.findOne();
customer.setInactive(true);
database.save(customer, txn);
txn.commit();
}
```
### Agent rule
If you are not deliberately bypassing thread-local transaction scope, do **not**
use `createTransaction()`. Most service code should use `@Transactional` or
`beginTransaction()`.
---
## Step 8 - Use bulk update/delete or JDBC batch for many-row writes
Loops of `database.save(...)` are often the wrong tool for large write sets.
### Prefer bulk update for set-based changes
If the update can be expressed as "change all rows matching this predicate",
perform one bulk update instead of loading and saving each bean.
### Example - bulk update with query beans
```java
var cust = QCustomer.alias();
int rows = new QCustomer()
.status.equalTo(Customer.Status.NEW)
.asUpdate()
.set(cust.status, Customer.Status.ACTIVE)
.update();
```
### Example - bulk update with `database.update(...)`
```java
int rows = database.update(Customer.class)
.set("status", Customer.Status.ACTIVE)
.where()
.eq("status", Customer.Status.NEW)
.update();
```
### Prefer JDBC batch for many individual inserts/updates
If each row has different values and must still go through per-bean persistence,
use batching.
```java
Database database = DB.getDefault();
try (Transaction txn = database.beginTransaction()) {
txn.setBatchMode(true);
txn.setBatchSize(100);
txn.setGetGeneratedKeys(false);
for (Customer customer : customersToInsert) {
database.insert(customer, txn);
}
txn.commit();
}
```
### Alternative - annotation-driven batching
```java
@Transactional(batchSize = 50)
public void importCustomers(List<Customer> customers) {
for (Customer customer : customers) {
database.insert(customer);
}
}
```
### Batch caveats
- Executing a query inside a batched transaction can flush the batch
- Mixing bean persistence and `SqlUpdate` can also flush the batch
- Accessing generated/unloaded properties on batched beans can flush the batch
If the workflow depends on delayed flushing, review the batch-flush rules before
adding more queries inside the same transaction.
---
## Common anti-patterns
### Anti-pattern 1 - Saving many rows one by one without batch or bulk update
If you are changing hundreds or thousands of rows, first ask whether it should
be a bulk update or a batched transaction.
### Anti-pattern 2 - Assuming child beans cascade automatically
Cascade is not automatic. Inspect the mapping first.
### Anti-pattern 3 - Wrapping external calls inside the database transaction
Do not keep transactions open while waiting on HTTP calls, queues, or other
slow external systems unless the design genuinely requires it.
### Anti-pattern 4 - Using `createTransaction()` for ordinary service code
Most service code should not bypass thread-local transaction handling.
### Anti-pattern 5 - Using `save()` when you really need `insert()` or `update()`
If operation intent matters, choose the more specific API.
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Child beans were not saved or deleted | Missing cascade mapping | Inspect annotations and add explicit save/delete or the correct cascade |
| Earlier writes committed even though later work failed | The whole workflow was not inside one transaction | Wrap the unit of work in `@Transactional` or an explicit transaction |
| `OptimisticLockException` on update/delete | Concurrent modification or stale version | Re-fetch, merge, or handle concurrency explicitly |
| Batch writes flush earlier than expected | Query, mixed SQL, or property access triggered flush | Review batch flush rules and transaction flow |
| Explicit transaction example does not affect the expected database | Mixed default DB and named DB usage | Use the same `Database` instance consistently for query and write |
---
## Summary workflow for AI agents
When asked to add persistence logic:
1. Choose `insert`, `save`, `update`, `delete`, or bulk update based on intent
2. Inspect cascade mappings before assuming related beans will persist/delete
3. Use implicit transactions for one isolated write
4. Use `@Transactional` for multi-step units of work
5. Use `beginTransaction()` only when explicit transaction control is needed
6. Use `createTransaction()` only for explicit, non-thread-local handling
7. Use bulk update or batching for large write sets
---
## Related documentation
- [Entity Bean Creation](entity-bean-creation.md)
- [Testing with TestEntityBuilder](testing-with-testentitybuilder.md)
- [Ebean persist docs](https://ebean.io/docs/persist)
- [Ebean transaction docs](https://ebean.io/docs/transactions)
@@ -1,817 +0,0 @@
# Guide: Testing with TestEntityBuilder
## Purpose
This guide explains how to use `TestEntityBuilder` to rapidly create test entity instances with auto-populated random values. It is written as practical instructions for developers and AI agents building tests for Ebean applications.
`TestEntityBuilder` eliminates boilerplate test setup by automatically generating realistic test data for all scalar fields, while respecting entity constraints and relationships. This is particularly valuable for:
- **Integration tests** that need representative data without caring about specific values
- **Persistence layer tests** that verify save/update/delete operations work correctly
- **Query and filter tests** where you need multiple entities with varied data
- **Rapid test setup** that reduces test code verbosity and improves readability
---
## Setup & Dependencies
### Add ebean-test to Your Project
The `TestEntityBuilder` class is provided by the `ebean-test` module.
**Maven:**
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
```
**Gradle:**
```gradle
testImplementation "io.ebean:ebean-test:${ebeanVersion}"
```
Use a version that matches your Ebean runtime (`ebean.version` /
`ebeanVersion`), or replace with an explicit fixed version if your build does
not centralize dependency versions.
> **Minimum version:** `TestEntityBuilder` was introduced in `ebean-test 17.5.0`. If your
> existing Ebean version is below this, upgrade before proceeding — mismatched Ebean
> runtime and test versions are not supported.
### Import the Class
```java
import io.ebean.test.TestEntityBuilder;
```
---
## Basic Usage
### Create a Builder Instance
`TestEntityBuilder` uses a builder pattern for configuration:
```java
TestEntityBuilder builder = TestEntityBuilder.builder(database).build();
```
The `Database` parameter specifies which Ebean database instance to use for entity type
lookups and persistence operations. Pass the injected `Database` bean (see
[Using with Dependency Injection](#using-with-dependency-injection) below) rather than
`DB.getDefault()` when working in a Spring or Avaje Inject context. For the same reason,
use the injected `database` bean for **all** persistence operations in your tests
(`database.save()`, `database.find()`, etc.) rather than mixing in static `DB.*` calls.
### Build an Entity (In-Memory)
The `build()` method creates an instance with populated fields **without persisting to the database:**
```java
Product product = builder.build(Product.class);
// Fields are populated:
// - id: unset (typically 0 for primitive long, null for boxed Long)
// - name: random UUID-based string
// - price: random BigDecimal
// - inStock: true
// - createdAt: current instant
// - etc.
// Not persisted yet (`@Id` is still unset until the entity is persisted).
```
### Build and Save (Persist to Database)
The `save()` method creates, persists, and returns an entity with the database-assigned `@Id`:
```java
Product product = builder.save(Product.class);
// Entity is now in the database:
assert database.find(Product.class, product.getId()) != null;
```
### Save Multiple Entities
The `saveAll()` method persists multiple pre-built entities in a single call:
```java
Product p1 = builder.build(Product.class);
Product p2 = builder.build(Product.class);
builder.saveAll(p1, p2);
// Both are now in the database with assigned IDs:
assert p1.getId() != null;
assert p2.getId() != null;
```
This is equivalent to `database.saveAll(p1, p2)` but avoids needing a separate
`Database` reference in tests that already hold a `TestEntityBuilder`.
### Access the Underlying Database
The `database()` method returns the `Database` instance used internally by the builder.
This is useful in tests where you want a single injected object (`TestEntityBuilder`) but
still need to perform `find()`, `delete()`, or other database operations:
```java
Product saved = builder.save(Product.class);
// Use builder.database() instead of injecting a separate Database bean:
Product found = builder.database().find(Product.class, saved.getId());
assert found != null;
```
---
## Using with Dependency Injection
Most applications using Ebean also use a DI framework. The recommended pattern is to
register `TestEntityBuilder` as a bean in the test DI context so it can be injected
directly into test classes — eliminating `@BeforeEach` setup boilerplate entirely.
### Avaje Inject — `@TestScope @Factory`
Add a `@Bean` method to your test-scoped `@Factory` class:
```java
import io.ebean.Database;
import io.ebean.test.ContainerDatabase;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.avaje.inject.test.TestScope;
import io.ebean.test.TestEntityBuilder;
@TestScope
@Factory
class TestConfiguration {
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("17") // Postgres image version
.dbName("my_app") // database to create inside the container
.build()
.start();
}
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.build();
}
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
```
Then inject it directly into test classes using `@InjectTest`:
```java
@InjectTest
class OrderControllerTest {
@Inject Database database;
@Inject TestEntityBuilder builder;
@Test
void findByStatus() {
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
database.save(order);
// ... test assertions
}
}
```
Both patterns produce a single shared `TestEntityBuilder` instance, wired
from the managed `Database` bean — no `@BeforeEach` required.
### Spring Boot — `@TestConfiguration`
Add a `@TestConfiguration` class that provides `TestEntityBuilder` as a bean:
```java
@TestConfiguration
class TestConfig {
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("17") // Postgres image version
.dbName("my_app") // database to create inside the container
.build()
.start();
}
// use @Primary if your main application context also wires a Database bean
// or conditionally wire the main Database bean to exclude it from tests
@Primary
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.build();
}
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
```
Then inject it directly into test classes:
```java
@SpringBootTest
class OrderControllerTest {
@Autowired Database database;
@Autowired TestEntityBuilder builder;
@Test
void findByStatus() {
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
database.save(order);
// ... test assertions
}
}
```
---
## Type-Specific Value Generation
`TestEntityBuilder` generates appropriate random values for each Java/SQL type. Customize this behavior by subclassing `RandomValueGenerator` (see "Custom Value Generators" below).
| Type | Generated Value | Notes |
|------|-----------------|-------|
| `String` | UUID-derived (8 chars by default) | Truncated to column length if `@Column(length=...)` is set |
| Email fields | `uuid@domain.com` format | Detected when property name contains "email" (case-insensitive) |
| `Integer` / `int` | Random in `[1, 1_000)` | |
| `Long` / `long` | Random in `[1, 100_000)` | |
| `Short` / `short` | Random in `[1, 100)` | See note on flag fields below |
| `Double` / `double` | Random in `[1, 100)` | |
| `Float` / `float` | Random in `[1, 100)` | |
| `BigDecimal` | Respects precision and scale | Precision and scale from `@Column(precision=..., scale=...)` |
| `Boolean` / `boolean` | `true` | Override in custom generator if needed |
| `UUID` | Random UUID | Via `UUID.randomUUID()` |
| `LocalDate` | Today's date | Via `LocalDate.now()` |
| `LocalDateTime` | Current datetime | Via `LocalDateTime.now()` |
| `Instant` | Current instant | Via `Instant.now()` |
| `OffsetDateTime` | Current time with zone | Via `OffsetDateTime.now()` |
| `ZonedDateTime` | Current time with zone | Via `ZonedDateTime.now()` |
| `Enum` | First constant | Override in custom generator if needed |
| Other types | `null` | Set these fields manually in tests |
### String Length Constraints
`TestEntityBuilder` respects column length constraints defined in the entity:
```java
@Entity
public class User {
@Column(length = 50)
private String username;
}
User user = builder.build(User.class);
assert user.getUsername().length() <= 50; // ✅ Constraint respected
```
### BigDecimal Precision and Scale
For `BigDecimal` fields, the builder respects the database column precision and scale:
```java
@Entity
public class LineItem {
@Column(precision = 10, scale = 2) // max 99_999_999.99
private BigDecimal amount;
}
LineItem item = builder.build(LineItem.class);
assert item.getAmount().scale() == 2;
```
### Short Fields Used as Boolean Flags
Some legacy schemas use `short` to represent boolean-like flags (e.g. `active = 1`
means active, `0` means inactive). `TestEntityBuilder` generates a random short in
`[1, 100)`, which will be non-zero but not necessarily `1`. If your application
code checks `entity.getActive() == 1` specifically, override the field after building:
```java
Organisation org = builder.build(Organisation.class)
.setActive((short) 1); // explicit override — random short won't do
```
---
## Entity Relationships
### Cascade-Persist Relationships: Recursively Built
Relationships marked with `cascade = PERSIST` are recursively populated:
```java
@Entity
public class Order {
@ManyToOne(cascade = CascadeType.PERSIST)
private Customer customer;
}
Order order = builder.build(Order.class);
// Both order and customer are built:
assert order != null;
assert order.getCustomer() != null;
// Before persist, @Id values are typically unset
// (0 for primitive IDs, null for boxed IDs).
// When saved, cascade handles both:
Order saved = builder.save(Order.class);
assert saved.getId() != null;
assert saved.getCustomer().getId() != null; // parent also saved
```
### Non-Cascade Relationships: Left Null
Relationships without cascade persist are not auto-created — even if marked `optional = false`.
Create and save the related entity first (the builder works well here), then assign it manually
before saving the parent:
```java
@Entity
public class BlogPost {
@ManyToOne
private Author author; // No cascade = left null by builder
}
BlogPost post = builder.build(BlogPost.class);
assert post.getAuthor() == null;
// Use the builder to create the related entity, then set it manually:
Author author = builder.save(Author.class);
post.setAuthor(author);
database.save(post);
```
### Collection Relationships: Left Empty
Collection relationships (`@OneToMany`, `@ManyToMany`) are left empty. On Ebean-enhanced
entities these fields are initialised to empty Ebean-managed lists (not `null`), so calling
`.add()` or `.addAll()` directly is safe:
```java
@Entity
public class Author {
@OneToMany(mappedBy = "author")
private List<BlogPost> posts; // Left empty
}
Author author = builder.build(Author.class);
assert author.getPosts().isEmpty();
// Populate if needed for testing:
author.getPosts().addAll(Arrays.asList(post1, post2, post3));
```
### Cycle Detection: Prevents Infinite Recursion
If two entities reference each other with cascade persist, the builder detects the cycle and breaks it by leaving one reference null:
```java
@Entity
public class Person {
@ManyToOne(cascade = CascadeType.PERSIST)
private Organization org;
}
@Entity
public class Organization {
@ManyToOne(cascade = CascadeType.PERSIST)
private Person founder;
}
Person person = builder.build(Person.class);
// One reference will be null to break the cycle:
// either person.org or person.org.founder is null
```
---
## Custom Value Generators
### Why Customize?
The default `RandomValueGenerator` uses generic random values. For domain-specific testing, you may want:
- Email addresses with your company domain
- Realistic phone numbers
- Product SKUs following a pattern
- Addresses in specific regions
- Monetary amounts within realistic ranges
### Creating a Custom Generator
Subclass `RandomValueGenerator` and override individual `random*()` methods:
```java
class CompanyTestDataGenerator extends RandomValueGenerator {
@Override
protected String randomString(String propName, int maxLength) {
if (propName != null && propName.toLowerCase().contains("email")) {
// Use company domain instead of generic @domain.com
String localPart = UUID.randomUUID().toString().substring(0, 8);
String email = localPart + "@mycompany.com";
if (maxLength > 0 && email.length() > maxLength) {
return email.substring(0, maxLength);
}
return email;
}
return super.randomString(propName, maxLength);
}
// Override other methods as needed:
@Override
protected Object randomEnum(Class<?> type) {
if (type == OrderStatus.class) {
// Bias towards common statuses for realistic test data
return ThreadLocalRandom.current().nextDouble() < 0.8
? OrderStatus.PENDING
: OrderStatus.COMPLETED;
}
return super.randomEnum(type);
}
}
```
### Using a Custom Generator
Pass the custom generator when building:
```java
TestEntityBuilder builder = TestEntityBuilder.builder(database)
.valueGenerator(new CompanyTestDataGenerator())
.build();
User user = builder.build(User.class);
assert user.getEmail().endsWith("@mycompany.com");
```
In a DI context, register this as the bean:
```java
// Spring Boot
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database)
.valueGenerator(new CompanyTestDataGenerator())
.build();
}
```
### Example: Money Type
```java
public class MoneyValueGenerator extends RandomValueGenerator {
@Override
protected BigDecimal randomBigDecimal(int precision, int scale) {
// Generate prices in a realistic range: $5.00 to $999.99
BigDecimal price = BigDecimal.valueOf(
ThreadLocalRandom.current().nextDouble(5.0, 1000.0)
);
return price.setScale(2, RoundingMode.HALF_UP);
}
}
```
---
## Best Practices
### 1. Use for Integration Tests, Not Unit Tests
**Good:** Integration test with database
```java
@Test
void whenSaving_thenCanRetrieve() {
Product product = builder.save(Product.class);
Product found = database.find(Product.class, product.getId());
assertThat(found).isNotNull();
}
```
**Poor:** Validation test requiring specific values
```java
@Test
void whenNameIsBlank_thenThrowException() {
Product product = builder.build(Product.class); // name is random!
product.setName(""); // have to override anyway
// ... test proceeds
}
```
### 2. Override Values for Specific Test Scenarios
When test requirements demand specific field values, manually override after building:
```java
@Test
void whenStockIsLow_thenShowWarning() {
Product product = builder.build(Product.class);
product.setQuantity(2); // Specific value for this test
boolean shouldWarn = product.shouldShowLowStockWarning();
assertThat(shouldWarn).isTrue();
}
```
### 3. Create Fixture Factories for Common Patterns
For shared domain-specific setup, encapsulate build patterns in an instance helper class
rather than a static factory. In a DI context, this class can be registered as a bean
alongside `TestEntityBuilder`:
```java
// Spring Boot
@TestConfiguration
class TestConfig {
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
@Bean
OrderTestFactory orderTestFactory(TestEntityBuilder builder, Database database) {
return new OrderTestFactory(builder, database);
}
}
public class OrderTestFactory {
private final TestEntityBuilder builder;
private final Database database;
public OrderTestFactory(TestEntityBuilder builder, Database database) {
this.builder = builder;
this.database = database;
}
public Order savePendingOrder() {
Order order = builder.build(Order.class);
order.setStatus(OrderStatus.PENDING);
database.save(order);
return order;
}
public Order saveShippedOrder() {
Order order = builder.build(Order.class);
order.setStatus(OrderStatus.SHIPPED);
order.setShippedAt(Instant.now());
database.save(order);
return order;
}
}
// Usage in tests:
@SpringBootTest
class OrderControllerTest {
@Autowired OrderTestFactory orderFactory;
@Test
void whenOrderPending_thenCanUpdate() {
Order order = orderFactory.savePendingOrder();
// ... test logic
}
}
```
### 4. Build Multiple Distinct Instances
Each call to `build()` or `save()` produces a new instance with fresh random values:
```java
@Test
void whenFetchingMultipleOrders_thenAllUnique() {
Order order1 = builder.save(Order.class);
Order order2 = builder.save(Order.class);
Order order3 = builder.save(Order.class);
assertThat(order1.getId()).isNotEqualTo(order2.getId());
assertThat(order2.getId()).isNotEqualTo(order3.getId());
assertThat(order1.getOrderNumber()).isNotEqualTo(order2.getOrderNumber());
}
```
---
## Complete Examples
### Example 1: Integration Test with Spring Boot
Register `TestEntityBuilder` as a `@TestConfiguration` bean, then inject it alongside
the repository under test:
```java
@TestConfiguration
class TestConfig {
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
@SpringBootTest
class OrderRepositoryTest {
@Autowired OrderRepository orderRepository;
@Autowired TestEntityBuilder builder;
@Test
void whenFindingOrdersByStatus_thenReturnsMatching() {
Order pending1 = builder.build(Order.class);
pending1.setStatus(OrderStatus.PENDING);
Order pending2 = builder.build(Order.class);
pending2.setStatus(OrderStatus.PENDING);
Order shipped = builder.build(Order.class);
shipped.setStatus(OrderStatus.SHIPPED);
builder.saveAll(pending1, pending2, shipped);
List<Order> pending = orderRepository.findByStatus(OrderStatus.PENDING);
assertThat(pending).hasSize(2);
}
}
```
### Example 2: Integration Test with Avaje Inject
```java
@TestScope
@Factory
class TestConfiguration {
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
@InjectTest
class OrderControllerTest {
@Inject TestEntityBuilder builder;
@Test
void whenFindingOrdersByStatus_thenReturnsMatching() {
Order pending1 = builder.build(Order.class);
pending1.setStatus(OrderStatus.PENDING);
Order pending2 = builder.build(Order.class);
pending2.setStatus(OrderStatus.PENDING);
Order shipped = builder.build(Order.class);
shipped.setStatus(OrderStatus.SHIPPED);
builder.saveAll(pending1, pending2, shipped);
// ... test assertions
}
}
```
### Example 3: Recursive Relationship Building
```java
@Test
void whenBuildingOrderWithCustomer_thenBothPopulated() {
Order order = builder.build(Order.class);
// Customer is recursively built because of @ManyToOne(cascade=PERSIST)
assertThat(order.getCustomer()).isNotNull();
// Before persist, @Id values are typically unset
// (0 for primitive IDs, null for boxed IDs).
assertThat(order.getCustomer().getName()).isNotNull();
// Saving cascades to customer:
Order saved = builder.save(Order.class);
assertThat(saved.getId()).isNotNull();
assertThat(saved.getCustomer().getId()).isNotNull();
}
```
### Example 4: Custom Generator for Domain Values
```java
// Custom generator for your domain
class ECommerceTestDataGenerator extends RandomValueGenerator {
@Override
protected BigDecimal randomBigDecimal(int precision, int scale) {
// Product prices typically range $10-$500
return BigDecimal.valueOf(
ThreadLocalRandom.current().nextDouble(10.0, 500.0)
).setScale(2, RoundingMode.HALF_UP);
}
}
@Test
void usingCustomGenerator() {
TestEntityBuilder builder = TestEntityBuilder.builder(database)
.valueGenerator(new ECommerceTestDataGenerator())
.build();
Product product = builder.build(Product.class);
assertThat(product.getPrice())
.isBetween(BigDecimal.TEN, BigDecimal.valueOf(500.0));
}
```
---
## Troubleshooting
### "No BeanDescriptor found for [Class] — is it an @Entity?"
**Cause:** The class you're trying to build is not registered as an Ebean entity.
**Solution:** Ensure the class is annotated with `@Entity` and registered with the Database:
```java
@Entity
@Table(name = "products")
public class Product {
// ...
}
```
### Fields are unset even though I expected them to be populated
**Cause:** `TestEntityBuilder` does **not** populate:
- `@Id` fields (identity/primary key; left unset until persist)
- `@Version` fields (optimistic locking; left unset until persist)
- `@Transient` fields
- `@OneToMany` collections
- Non-cascade `@ManyToOne` relationships
**Solution:** Set only the fields your test scenario cares about, then persist.
`@Id` and `@Version` are usually database-managed and should typically be left
unset before save:
```java
Product product = builder.build(Product.class);
product.setName("specific-name"); // test-specific override
database.save(product); // database assigns @Id/@Version
```
### Building recursive relationships causes StackOverflowError
**Cause:** Two or more entities mutually reference each other without cycle detection.
**Solution:** This should be handled automatically by cycle detection. If not, manually set one reference to null:
```java
Person person = builder.build(Person.class);
person.getOrganization().setFounder(null); // Break cycle
```
### Values generated are "too random" for my test
**Cause:** Default `RandomValueGenerator` uses true random values, which aren't suitable when your test needs predictable data.
**Solution:** Create a custom generator that produces deterministic values:
```java
class DeterministicTestDataGenerator extends RandomValueGenerator {
private int counter = 0;
@Override
protected String randomString(String propName, int maxLength) {
return "test_" + (counter++);
}
}
```
---
## Summary
`TestEntityBuilder` accelerates test development by:
1. **Reducing boilerplate** — No need to manually set every field
2. **Improving readability** — Tests focus on what matters, not setup
3. **Enabling variety** — Each build produces distinct random values
4. **Respecting constraints** — Column lengths and decimal scales are enforced
5. **Supporting customization** — Extend `RandomValueGenerator` for domain needs
-464
View File
@@ -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/)
-598
View File
@@ -1,598 +0,0 @@
# Guide: Write Ebean Queries with Query Beans
## Purpose
This guide gives step-by-step instructions for AI agents and developers to write
application queries using Ebean query beans.
Use this guide when the project already has Ebean configured and you need to:
- add a repository/service query
- replace string-based ORM queries with type-safe query beans
- tune what data is fetched to avoid over-fetching or N+1 issues
- return DTO projections for list screens or API responses
The default recommendation is:
1. Prefer query beans first
2. Prefer entity queries for domain logic
3. For read-only entity graphs, prefer `setUnmodifiable(true)`
4. Prefer DTO projection for summary/read-model use cases
5. Only drop to raw SQL when the ORM query cannot express the requirement cleanly
---
## Prerequisites
- The project already uses Ebean ORM
- Query bean generation is configured (for Maven this usually means
`querybean-generator` is registered as an annotation processor)
- Entity beans already exist
- A compile/build has run successfully since the last entity model change
If query beans are not yet configured, first follow:
[`add-ebean-postgres-maven-pom.md`](add-ebean-postgres-maven-pom.md)
---
## Step 1 - Verify the generated `Q*` query bean exists
For each entity bean, Ebean generates a query bean with the same name prefixed
with `Q`.
Examples:
- `Customer` -> `QCustomer`
- `Order` -> `QOrder`
- `Contact` -> `QContact`
Import the generated type from the query bean package:
```java
import org.example.domain.query.QCustomer;
```
If the `Q*` type does not exist or the IDE cannot resolve it:
1. Confirm the entity compiled successfully
2. Run a normal project compile/build
3. If the entity was renamed or moved, run a full rebuild rather than relying on
incremental compilation
### Important caveat - entity rename
After refactoring an entity name, old generated query beans can remain on disk
until the next full build. If both old and new `Q*` types appear to exist, do a
clean rebuild before editing application queries.
---
## Step 2 - Choose the terminal query method before writing predicates
Decide what the caller actually needs. This determines the terminal method and
often the right query shape.
| Need | Preferred method | Notes |
|------|------------------|-------|
| Check if at least one row exists | `exists()` | Cheapest choice for boolean existence checks |
| Load exactly one row by ID or unique key | `findOne()` | Only use when the predicate is truly unique |
| Load a list of entity beans | `findList()` | Default for list screens and domain logic |
| Stream rows, usually to map into another type | `findStream()` | For large/unbounded results streamed from the JDBC cursor; close via try-with-resources. For small/bounded results prefer `findList().stream()` |
| Count matching rows | `findCount()` | Prefer over loading entities just to count |
| Load a page plus optional total row count | `findPagedList()` | Use when the caller needs pagination metadata |
| Return DTO/read-model rows | `asDto(...).findList()` | Prefer this over partially loaded entities for API/view models |
### Example - existence check
```java
boolean alreadyUsed = new QCustomer()
.email.equalTo(email)
.exists();
```
### Example - unique lookup
```java
Customer customer = new QCustomer()
.email.equalTo(email)
.findOne();
```
Do **not** use `findOne()` for predicates that can match multiple rows.
### Example - stream and map to another type
Choose based on result size and how you consume it:
- **`findList().stream()`** — executes the query, materialises the rows,
**releases the connection**, then streams over an in-memory list. No open
database resources and no try-with-resources needed. Prefer this for small or
bounded results (e.g. when you apply `setMaxRows`) that you collect anyway.
- **`findStream()`** — streams rows directly from the JDBC cursor, holding a
connection (and an implicit transaction) open for the **whole lifetime of the
stream pipeline**. It must be closed with try-with-resources. Prefer it when
the result may be large, when you want constant memory, or when you want to
short-circuit (`limit`, `findFirst`, `takeWhile`) without loading everything.
```java
// small, bounded result fully collected -> findList().stream()
List<PendingPlan> pending = new QCaptureRequest()
.collectedAt.isNull()
.orderBy().requestedAt.asc()
.findList()
.stream()
.map(r -> new PendingPlan(r.app().getName(), r.hash()))
.toList();
// large/unbounded result streamed from the cursor -> findStream() + try-with-resources
try (Stream<Customer> stream = new QCustomer()
.status.equalTo(Status.NEW)
.findStream()) {
stream
.map(...)
.forEach(...);
}
```
For processing large results one bean at a time, `findEach()` is often the
simplest choice because it closes the underlying resources automatically.
---
## Step 3 - Build predicates by traversing properties and associations
With query beans, write predicates directly against properties. When you
traverse an association, Ebean adds the necessary joins automatically.
### Example - root property predicates
```java
List<Customer> customers = new QCustomer()
.status.equalTo(Customer.Status.ACTIVE)
.name.istartsWith("rob")
.findList();
```
### Example - association traversal
```java
List<Customer> customers = new QCustomer()
.billingAddress.city.equalTo("Auckland")
.findList();
```
### Example - collection predicate
```java
List<Customer> customers = new QCustomer()
.contacts.isEmpty()
.findList();
```
### Optional predicates - prefer conditional helpers over `if` blocks
When a filter is driven by a nullable/optional parameter, use the built-in
conditional helpers instead of wrapping predicates in `if` blocks. The query
stays fluent and reads top-to-bottom, and no predicate is added when the value
is absent.
| Helper | Adds predicate when | Resulting SQL |
|--------|---------------------|---------------|
| `eqIfPresent(v)` | `v != null` | `prop = ?` |
| `eqIfNotBlank(v)` (String) | `v` non-null and not blank (value is trimmed) | `prop = ?` |
| `eqOrNull(v)` | always | `(prop = ? or prop is null)` |
| `inOrEmpty(coll)` | `coll` non-empty | `prop in (...)` (no predicate when empty) |
| `likeIfPresent` / `ilikeIfPresent` / `startsWithIfPresent` / `istartsWithIfPresent` / `containsIfPresent` / `icontainsIfPresent` (String) | `v != null` | the match expression |
```java
// Instead of building the query with if blocks:
QCustomer q = new QCustomer();
if (name != null && !name.isBlank()) {
q.name.eq(name.trim());
}
if (status != null) {
q.status.eq(status);
}
List<Customer> customers = q.findList();
// Prefer the conditional helpers:
List<Customer> customers = new QCustomer()
.name.eqIfNotBlank(name)
.status.eqIfPresent(status)
.findList();
```
Use `eqOrNull(v)` when a null column value should also match - for example an
"any environment" row stored with `env_id is null` should surface under any env
filter - instead of a hand-rolled `or()/eq()/isNull()/endOr()` block:
```java
List<CaptureRequest> rows = new QCaptureRequest()
.env.name.eqOrNull(envFilter) // env_name = ? or env_name is null
.findList();
```
### Agent rule
When adding a new query:
1. Start from the root entity that the caller wants back
2. Add predicates with query bean properties
3. Traverse relationships instead of writing manual join SQL
4. Keep property references type-safe; avoid string property names unless the API
specifically requires them
5. For optional filters, reach for `eqIfPresent` / `eqIfNotBlank` / `inOrEmpty`
before writing an `if (param != null)` block, and use `eqOrNull` instead of a
manual `or()/eq()/isNull()/endOr()` when the intent is "match this value or a
null column"
---
## Step 4 - Add ordering, limits, and pagination deliberately
Do not leave list queries unordered unless the call site truly does not care.
For UI lists, APIs, and background jobs, explicit ordering is usually better.
### Example - ordered list with limit
```java
List<Customer> customers = new QCustomer()
.status.equalTo(Customer.Status.ACTIVE)
.orderBy().name.asc()
.setMaxRows(50)
.findList();
```
### Example - offset/limit pagination
```java
List<Customer> customers = new QCustomer()
.status.equalTo(Customer.Status.ACTIVE)
.orderBy().id.asc()
.setFirstRow(offset)
.setMaxRows(pageSize)
.findList();
```
### Example - paged list with total count
```java
PagedList<Customer> page = new QCustomer()
.status.equalTo(Customer.Status.ACTIVE)
.orderBy().id.asc()
.setFirstRow(offset)
.setMaxRows(pageSize)
.findPagedList();
page.loadRowCount();
List<Customer> customers = page.getList();
int totalRowCount = page.getTotalRowCount();
```
### Agent rule
- Use `findList()` when the caller only needs rows
- Use `findPagedList()` when the caller also needs page metadata or total counts
- Pair pagination with a stable `orderBy()` so page boundaries stay predictable
---
## Step 5 - Control fetched data with `select()` and `fetch()`
By default, entity queries can load more of the object graph than the caller
actually needs. Use `select()` and `fetch()` to control the root and association
properties that are loaded.
### Root properties with `select()`
Use `select()` to define which properties should be fetched on the root entity.
### Associated bean properties with `fetch()`
Use `fetch()` to define what should be fetched on associated paths.
### Example - partial entity query
```java
private static final QCustomer CUST = QCustomer.alias();
private static final QContact CONT = QContact.alias();
List<Customer> customers = new QCustomer()
.select(CUST.name, CUST.status, CUST.whenCreated)
.contacts.fetch(CONT.email)
.name.istartsWith("rob")
.findList();
```
In this example:
- `select(...)` tunes the root `Customer` properties
- `contacts.fetch(...)` tunes the associated `Contact` properties
- the query still returns `Customer` entity beans
### Agent rules for partial entity queries
1. Only use `select()`/`fetch()` when you know what the caller will read next
2. Do not treat partially loaded entities like fully populated API DTOs
3. If the caller only needs summary fields, prefer a DTO projection instead
---
## Step 6 - Use `setUnmodifiable(true)` for read-only entity graphs
`setUnmodifiable(true)` turns the returned object graph into an unmodifiable,
read-only graph.
This means:
- setters cannot mutate returned beans
- associated collections are unmodifiable
- lazy loading is disabled
- accessing an unloaded property throws `LazyInitialisationException`
- the query uses `PersistenceContextScope.QUERY`
### Example - read-only entity graph
```java
private static final QCustomer CUST = QCustomer.alias();
private static final QContact CONT = QContact.alias();
List<Customer> customers = new QCustomer()
.select(CUST.name, CUST.status, CUST.whenCreated)
.contacts.fetch(CONT.email)
.status.equalTo(Customer.Status.ACTIVE)
.setUnmodifiable(true)
.findList();
```
### When to prefer `setUnmodifiable(true)`
Use it when the result is meant to be read-only, such as:
- service/query methods returning entity graphs for display or serialization
- query results you want the application to treat as immutable
- cached query results or other shared read models backed by entity graphs
- partial entity graphs where you want accidental lazy loading to fail fast
### When **not** to use it
Do **not** use `setUnmodifiable(true)` when the caller will:
- modify the beans and save them later
- rely on lazy loading of associations or unloaded scalar properties
- treat the result as a working persistence model rather than a read-only view
### Agent rule
If you are returning entity beans for read-only use, `setUnmodifiable(true)`
should be the default recommendation. If the caller needs a mutable model or a
serialized summary shape, choose mutable entities or DTO projection instead.
If you need cached assoc-one references for unmodifiable graphs, see
[Immutable bean cache for read-only references](immutable-bean-cache.md).
---
## Step 7 - Use `fetchQuery()` for to-many paths and `FetchGroup` for reusable query shapes
Ebean applies important SQL rules when translating ORM queries:
1. It does not generate SQL cartesian products
2. It honors `maxRows` in SQL
This means to-many paths often need special handling.
### Use `fetchQuery()` when:
- the query includes a `OneToMany` or `ManyToMany` path
- the query includes `setMaxRows(...)`
- the query loads multiple to-many paths
- you want the query shape to make the secondary-query behavior explicit
### Example - explicit secondary queries for to-many paths
```java
private static final QCustomer CUST = QCustomer.alias();
List<Order> orders = new QOrder()
.customer.fetch(CUST.name)
.lines.fetchQuery()
.shipments.fetchQuery()
.status.equalTo(Order.Status.NEW)
.setMaxRows(100)
.findList();
```
### Use `FetchGroup` when:
- the same fetch shape is reused in multiple places
- you want to separate predicate logic from fetch-shape tuning
- you want an immutable, static query-shape definition
### Example - reusable fetch group
```java
private static final QCustomer CUST = QCustomer.alias();
private static final FetchGroup<Customer> CUSTOMER_SUMMARY =
QCustomer.forFetchGroup()
.select(CUST.name, CUST.status, CUST.whenCreated)
.billingAddress.fetch()
.buildFetchGroup();
List<Customer> customers = new QCustomer()
.select(CUSTOMER_SUMMARY)
.status.equalTo(Customer.Status.ACTIVE)
.findList();
```
### Agent rule
If the caller needs multiple to-many paths or a paged query, be suspicious of a
plain `fetch(...)` on those paths. `fetchQuery()` is often the safer default.
---
## Step 8 - Use DTO projection when the caller does not need entity beans
For list screens, API summaries, exports, or read-model views, the caller often
does **not** need managed entity beans. In those cases, project directly to a
DTO using `asDto(...)`.
### Example - DTO projection with query beans
```java
import static org.example.domain.query.QCustomer.Alias.id;
import static org.example.domain.query.QCustomer.Alias.name;
public record CustomerSummary(long id, String name) {}
List<CustomerSummary> summaries = new QCustomer()
.select(id, name)
.status.equalTo(Customer.Status.ACTIVE)
.orderBy().name.asc()
.asDto(CustomerSummary.class)
.findList();
```
### Prefer DTO projection when:
- the caller will serialize the result directly
- only a subset of fields is needed
- the result is not going to be updated and saved back as an entity
- the query contains formulas or aggregation intended for a read model
---
## Step 9 - Only fall back to raw SQL when the ORM query is not a good fit
Prefer the following order:
1. Query bean query
2. Query bean query + `asDto(...)`
3. `database.findDto(...)` or DTO query
4. Native SQL / `SqlQuery` / `RawSql`
### Typical reasons to use raw SQL
- vendor-specific SQL that query beans do not express well
- advanced aggregation or database functions
- hand-tuned reporting queries
- stored procedures or raw JDBC workflows
Do **not** jump to raw SQL just because the query joins multiple tables. Query
beans already handle ordinary relationship traversal well.
### Using `RawSql` with query beans
`RawSql` is not limited to the plain `Query<T>` API - it also works with a
generated query bean, giving type-safe `where()`/`having()` expressions over
hand-written SQL. Every generated query bean exposes `setRawSql(...)`:
```java
RawSql rawSql = RawSqlBuilder.parse("select id, name, status from customer")
.columnMapping("id", "id")
.columnMapping("name", "name")
.columnMapping("status", "status")
.create();
List<Customer> customers = new QCustomer()
.setRawSql(rawSql)
.status.equalTo(Customer.Status.ACTIVE) // typed expression, injected into the parsed WHERE clause
.findList();
```
For the full guide to building `RawSql` - including `unparsed()`,
`withPlaceholders()` for CTEs/window functions, the `${where}` / `${andWhere}`
/ `${having}` / `${andHaving}` placeholder reference, and column mapping - see
[Using `RawSql` with Ebean](using-rawsql-with-ebean.md).
---
## Common anti-patterns
### Anti-pattern 1 - Using raw SQL first
**Avoid:**
```java
List<Customer> customers = database.findNative(Customer.class,
"select c.* from customer c join address a on a.id = c.billing_address_id where a.city = ?")
.setParameter(1, city)
.findList();
```
**Prefer:**
```java
List<Customer> customers = new QCustomer()
.billingAddress.city.equalTo(city)
.findList();
```
### Anti-pattern 2 - Using `findOne()` on a non-unique predicate
**Avoid:**
```java
Customer customer = new QCustomer()
.status.equalTo(Customer.Status.ACTIVE)
.findOne();
```
**Why:** Many rows can match; this is not a unique lookup.
### Anti-pattern 3 - Returning partially loaded entities as API models
If the caller only needs summary fields, return a DTO instead of partially
loaded entities that might later trigger more loading or confuse serializers.
### Anti-pattern 4 - Returning mutable entity graphs for read-only use
If the caller is only meant to read the result, prefer `setUnmodifiable(true)`
so accidental setter calls, collection mutation, and lazy loading fail fast.
### Anti-pattern 5 - Fetching every relationship "just in case"
Do not eagerly fetch large object graphs unless the immediate caller will use
them. Query tuning is part of the job.
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `Cannot resolve symbol QCustomer` | Query bean generation not configured or build not run | Check the annotation processor and run a build |
| Old `Q*` class still appears after entity rename | Stale generated source/class output | Run a clean rebuild |
| `findOne()` fails because multiple rows match | Predicate is not unique | Use `findList()` or tighten the predicate |
| Returned entities only have some fields loaded | `select()` or `FetchGroup` limited the query shape | Add the required fields or switch to DTO projection |
| Setter calls or collection mutation fail on query results | `setUnmodifiable(true)` returned a read-only graph | Remove `setUnmodifiable(true)` or treat the result as read-only |
| Accessing an unloaded property throws `LazyInitialisationException` | `setUnmodifiable(true)` disables lazy loading | Fetch the property up front or use DTO projection |
| Ebean executes secondary queries for a to-many path | ORM rules avoided cartesian product or honored `maxRows` | This is expected; use `fetchQuery()` explicitly when appropriate |
---
## Summary workflow for AI agents
When asked to add or modify an Ebean query:
1. Verify the relevant `Q*` type exists
2. Choose the terminal method first (`exists`, `findOne`, `findList`, `findPagedList`, `asDto`)
3. Add predicates with query bean properties and association traversal
4. Add explicit ordering and pagination if relevant
5. If the result is read-only entity data, consider `setUnmodifiable(true)`
6. Tune the fetch shape with `select()` / `fetch()` / `fetchQuery()` / `FetchGroup`
7. Prefer DTO projection for read models and serialized responses
8. Only use raw SQL if the ORM query is genuinely the wrong tool
---
## Related documentation
- [Add Ebean Postgres Maven POM](add-ebean-postgres-maven-pom.md)
- [Entity Bean Creation](entity-bean-creation.md)
- [Immutable bean cache for read-only references](immutable-bean-cache.md)
- [Using `RawSql` with Ebean](using-rawsql-with-ebean.md)
- [Ebean query docs](https://ebean.io/docs/query/)
-333
View File
@@ -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
-155
View File
@@ -1,155 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ebean-parent</artifactId>
<groupId>io.ebean</groupId>
<version>18.2.0</version>
</parent>
<name>ebean api</name>
<description>ebean api</description>
<artifactId>ebean-api</artifactId>
<dependencies>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-applog</artifactId>
<version>1.0</version>
</dependency>
<!-- exclude avaje-applog-slf4j to direct logging to something else -->
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-applog-slf4j</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-config</artifactId>
<version>4.2</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>jakarta-persistence-api</artifactId>
<version>${ebean-persistence-api.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-annotation</artifactId>
<version>${ebean-annotation.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-types</artifactId>
<version>${ebean-types.version}</version>
</dependency>
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-datasource-api</artifactId>
<version>${ebean-datasource.version}</version>
</dependency>
<!-- Support MdcBackgroundExecutorWrapper -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-json-core</artifactId>
<version>${avaje-json-core.version}</version>
</dependency>
<!-- Jackson databind remains for ObjectMapper compatibility paths -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/ebean-maven-version.txt</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/ebean-maven-version.txt</exclude>
</excludes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>11</release>
</configuration>
</execution>
<execution>
<id>compile-21</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>21</release>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java21</compileSourceRoot>
</compileSourceRoots>
<multiReleaseOutput>true</multiReleaseOutput>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
<!-- <manifest>-->
<!-- <addDefaultImplementationEntries>true</addDefaultImplementationEntries>-->
<!-- </manifest>-->
</plugin>
</plugins>
</build>
</project>
@@ -1,91 +0,0 @@
package io.ebean;
import org.jspecify.annotations.NullMarked;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Background executor service for executing of tasks asynchronously.
* <p>
* This service can be used to execute tasks in the background.
* <p>
* This service is managed by Ebean and will perform a clean shutdown
* waiting for background tasks to complete with a default 30 second
* timeout. Shutdown occurs prior to DataSource shutdown.
* <p>
* This also propagates MDC context from the current thread to the
* background task if defined.
*/
@NullMarked
public interface BackgroundExecutor {
/**
* Execute a callable task in the background returning the Future.
*/
<T> Future<T> submit(Callable<T> task);
/**
* Execute a runnable task in the background returning the Future.
*/
Future<?> submit(Runnable task);
/**
* Execute a task in the background. Effectively the same as
* {@link BackgroundExecutor#submit(Runnable)} but returns void.
*/
void execute(Runnable task);
/**
* Execute a task periodically with a given delay.
*
* @param task the task to execute
* @param initialDelay the time to delay first execution
* @param delay the delay between the termination of one
* execution and the commencement of the next
* @param unit the time unit of the initialDelay and delay parameters
* @return a ScheduledFuture representing pending completion of
* the series of repeated tasks. The future's {@link
* Future#get() get()} method will never return normally,
* and will throw an exception upon task cancellation or
* abnormal termination of a task execution.
*/
ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit);
/**
* Execute a task periodically with a given period.
*
* <p>If any execution of this task takes longer than its period, then
* subsequent executions may start late, but will not concurrently
* execute.
*
* @param task the task to execute
* @param initialDelay the time to delay first execution
* @param period the period between successive executions
* @param unit the time unit of the initialDelay and period parameters
* @return a ScheduledFuture representing pending completion of
* the series of repeated tasks. The future's {@link
* Future#get() get()} method will never return normally,
* and will throw an exception upon task cancellation or
* abnormal termination of a task execution.
*/
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit);
/**
* Schedules a Runnable for one-shot action that becomes enabled after the given delay.
*
* @return a ScheduledFuture representing pending completion of the task and
* whose get() method will return null upon completion
*/
ScheduledFuture<?> schedule(Runnable task, long delay, TimeUnit unit);
/**
* Schedules a Callable for one-shot action that becomes enabled after the given delay.
*
* @return a ScheduledFuture that can be used to extract result or cancel
*/
<V> ScheduledFuture<V> schedule(Callable<V> task, long delay, TimeUnit unit);
}
@@ -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,19 +0,0 @@
package io.ebean;
/**
* Defines a cancelable query.
* <p>
* Typically holds a representation of the PreparedStatement to perform the
* actual cancel.
* </p>
*/
public interface CancelableQuery {
/**
* Cancel the query.
* <p>
* For JDBC this translates to calling cancel on the PreparedStatement.
* </p>
*/
void cancel();
}

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