Docs: update docs / skills location to separate git repo

This commit is contained in:
robin.bygrave
2026-05-01 15:38:37 +12:00
parent f07ca68eb3
commit 50e20c3aa0
10 changed files with 21 additions and 4133 deletions
+17
View File
@@ -65,6 +65,23 @@ useful for production deployments, especially in Kubernetes or AWS environments:
## 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
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.
+4 -2
View File
@@ -159,13 +159,15 @@ Example — if the existing block already has a path for, say, `avaje-nima-gener
## Verification
Run the following to confirm the POM is valid and the project compiles:
Run the following to confirm the POM is valid and both main and test sources compile:
```bash
mvn compile -pl <your-module-name>
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.
---
-104
View File
@@ -1,104 +0,0 @@
---
name: ebean-orm
description: Ebean ORM guidance for entity modeling, querying, persistence, testing, setup, and migrations. Use when working with Ebean entities, queries, database code, or project setup.
---
# Ebean ORM
Ebean is a Java/Kotlin ORM for SQL databases with compile-time enhancement, query beans, and zero-boilerplate entity modeling.
## Key Principles
- Ebean enhances entity beans at compile time — no runtime proxies
- Fields must be non-public (private, protected, or package-private)
- Add getters/setters for all fields that application code needs to read or write
- No default constructor, equals/hashCode, or toString required (all enhanced automatically)
- Prefer query beans (`QCustomer`) for type-safe queries
- Prefer primitive `long` for `@Id` and `@Version` fields
- Use `@WhenCreated` / `@WhenModified` for audit timestamps
- Use a `Database` instance (`database.save()`, `database.find()`) not static `DB.*` methods
## Task Guides
Load the relevant reference guide for the current task. **Only load what you need** — don't read all of them at once.
| Task | Reference |
|------|-----------|
| Project setup, dependencies, config | [setup](references/setup.md) |
| Entity modeling, relationships, Lombok | [modeling](references/modeling.md) |
| Queries, query beans, fetch tuning, DTOs | [querying](references/querying.md) |
| Persist, save, update, delete, transactions | [write-transactions](references/write-transactions.md) |
| Testing patterns and tools | [testing](references/testing.md) |
| DB migration generation and execution | [db-migrations](references/db-migrations.md) |
## Quick Reference
### Entity Pattern
```java
@Entity
public class Customer {
@Id long id;
@Version long version;
@WhenCreated Instant whenCreated;
@WhenModified Instant whenModified;
String name;
String email;
@OneToMany(mappedBy = "customer")
List<Order> orders;
public long getId() { return id; }
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; }
}
```
### Query Bean Pattern
```java
var customers = new QCustomer()
.name.istartsWith("rob")
.orders.status.eq(OrderStatus.ACTIVE)
.setMaxRows(50)
.findList();
```
### Save/Persist
```java
var customer = new Customer();
customer.setName("Alice");
customer.setEmail("alice@example.com");
database.save(customer);
```
### Transactions
```java
try (Transaction txn = database.beginTransaction()) {
var order = new Order();
order.setCustomer(customer);
database.save(order);
var item = new OrderItem(order, product, qty);
database.save(item);
txn.commit();
}
```
## Regenerating References
The reference files in `references/` are generated from the source guides in
`docs/guides/`. To regenerate after editing guides:
```bash
./docs/skill/generate-references.sh
```
Commit the updated references alongside the guide changes.
@@ -1,374 +0,0 @@
# Ebean ORM Bundle — DB Migrations (Flattened)
> Flattened bundle. Content from source markdown guides is inlined below.
---
## Source: `add-ebean-db-migration-generation.md`
# Guide: Add Ebean Database Migration Generation to an Existing Maven Project
## Purpose
This guide provides step-by-step instructions for adding Ebean DB migration generation
to an existing Maven project that already uses Ebean ORM. Ebean generates migrations by
performing a diff of the current entity model against the previously recorded model state,
producing platform-specific DDL SQL scripts.
These instructions are designed for AI agents and developers to follow precisely.
---
## Prerequisites
- An existing Maven project with Ebean ORM configured (entity beans present)
- `ebean-test` is already a test-scoped dependency (from POM setup guide)
- The project targets PostgreSQL (adjust `Platform.POSTGRES` for other databases)
---
## Step 1 — Verify migration dependencies
### Generation tooling (`ebean-ddl-generator`)
`ebean-test` (already present as a test dependency) transitively includes
`ebean-ddl-generator`, which provides the `DbMigration` class. No additional dependency
is required for generation.
### Runtime migration runner (`ebean-migration`)
`ebean-migration` is the library that runs migrations on application startup.
It is typically included **transitively** via `io.ebean:ebean-postgres` (or the
equivalent platform dependency). Verify it is on the classpath by running:
```bash
mvn dependency:tree | grep ebean-migration
```
If it is **not** present transitively, add it explicitly as a compile-scope dependency:
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean.version}</version>
</dependency>
```
---
## Step 2 — Create `GenerateDbMigration.java`
Create the following class in `src/test/java/main/`. This `main` method is run manually
by a developer (or AI agent) whenever entity beans change and a new migration is needed.
```java
package main;
import io.ebean.annotation.Platform;
import io.ebean.dbmigration.DbMigration;
import java.io.IOException;
/**
* Generate the next database migration based on a diff of the entity model.
* Run this main method after making entity bean changes to produce the migration SQL.
*/
public class GenerateDbMigration {
public static void main(String[] args) throws IOException {
DbMigration migration = DbMigration.create();
migration.setPlatform(Platform.POSTGRES);
migration.setVersion("1.1"); // set to the next migration version
migration.setName("add-customer"); // short description of the change
migration.generateMigration();
}
}
```
### Version naming convention
Ebean supports two common version formats — choose one and apply it consistently:
| Format | Example | Notes |
|--------|---------|-------|
| **Date-based** | `20240820` | `YYYYMMDD`; used when changes are tied to dates; easily sortable |
| **Semantic** | `1.1`, `1.2`, `2.0` | Traditional versioning; useful for release-based workflows |
The version controls execution order — Ebean runs migrations in ascending version order.
### Name convention
The `name` should be a short, lowercase, hyphenated description of the change:
- `add-customer-email`
- `rename-machine-type`
- `drop-unused-columns`
---
## Step 3 — Configure the output path (if needed)
By default, migration files are written to `src/main/resources/dbmigration/` relative
to the **current working directory** when `generateMigration()` is called. This is
usually the module root, which is correct for single-module projects.
For **multi-module projects** where `GenerateDbMigration` is in a submodule but the
resources directory is at a different relative path, specify it explicitly:
```java
// Relative path from the working directory (project root) to the module's resources
migration.setPathToResources("my-module/src/main/resources");
```
---
## Step 4 — Run `GenerateDbMigration` to produce the first migration
Run the `main` method via the IDE or Maven:
```bash
# Run via Maven exec plugin (or use IDE run configuration)
mvn test-compile exec:java \
-Dexec.mainClass="main.GenerateDbMigration" \
-Dexec.classpathScope="test" \
-pl <your-module>
```
Ebean migration generation runs in **offline mode** — no database connection is required.
### Expected output files
After running, two files are created per migration in `src/main/resources/dbmigration/`:
```
src/main/resources/dbmigration/
1.1__add-customer.sql ← DDL SQL to apply (commit this)
model/
1.1__add-customer.model.xml ← logical model diff XML (commit this)
```
Both files must be committed to source control. The `.model.xml` file records the
logical state of the diff and is used by subsequent migration generations to determine
what has changed.
If **no entity beans have changed** since the last migration, the command outputs:
```
DbMigration - no changes detected - no migration written
```
---
## Step 5 — Enable the migration runner
Configure Ebean to run pending migrations automatically on application startup.
### Preferred approach — programmatic via `DatabaseBuilder`
Set `runMigration(true)` directly on the `DatabaseBuilder` when constructing
the `Database` bean. This is the preferred approach as it is explicit, co-located with
the database configuration, and does not rely on external property files.
In the `@Factory` class that builds the `Database` bean (see the database configuration
guide), add `.runMigration(true)` to the builder chain:
```java
@Bean
Database database(ConfigWrapper config) {
var dataSource = DataSourceBuilder.create()
.url(config.getDatabaseUrl())
.username(config.getDatabaseUser())
.password(config.getDatabasePassword())
// ... other datasource settings ...
;
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.runMigration(true) // run pending migrations on startup
.build();
}
```
If migrations should only run in certain environments (e.g., not in production, or
only when a config flag is set), make it conditional:
```java
.runMigration(config.isRunMigrations()) // driven by config value
```
### Alternative — via application properties
If programmatic configuration is not available or not preferred, set the property
in `src/main/resources/application.properties`:
```properties
ebean.migration.run=true
```
Or in `src/main/resources/application.yaml`:
```yaml
ebean:
migration:
run: true
```
For a **named database** (i.e., `Database.builder().name("mydb")`), use the database
name in the property key:
```properties
ebean.mydb.migration.run=true
```
### What the runner does at startup
When migration running is enabled, Ebean will on each application start:
1. Look at the migrations in `src/main/resources/dbmigration/`
2. Compare against the `db_migration` table (created automatically on first run)
3. Apply any migrations that have not yet been executed, in version order
4. Record each successfully applied migration in `db_migration`
---
## Step 6 — Commit the migration files
Add both generated files to source control:
```bash
git add src/main/resources/dbmigration/1.1__add-customer.sql
git add src/main/resources/dbmigration/model/1.1__add-customer.model.xml
git commit -m "Add db migration 1.1: add-customer"
```
---
## Ongoing workflow — generating subsequent migrations
For each future set of entity bean changes:
1. Make changes to the entity bean classes
2. Update `GenerateDbMigration.java` with the **new version** and **new name**:
```java
migration.setVersion("1.2");
migration.setName("add-address-table");
```
3. Run the `main` method — a new `.sql` and `.model.xml` pair is written
4. Review the generated `.sql` to confirm it reflects the intended changes
5. Commit both files
---
## Understanding the output files
### Apply SQL (`.sql`)
The apply SQL file contains the DDL that will be executed against the database:
```sql
-- apply changes
alter table customer add column email varchar(255);
```
### Model XML (`.model.xml`)
The model XML records the logical diff in a database-agnostic format. Ebean uses
this file on the next generation run to determine what has already been captured.
It is not executed against the database.
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<migration xmlns="http://ebean-orm.github.io/xml/ns/dbmigration">
<changeSet type="apply">
<addColumn tableName="customer">
<column name="email" type="varchar(255)"/>
</addColumn>
</changeSet>
</migration>
```
---
## Optional configurations
### Multiple database platforms
To generate migrations for multiple platforms simultaneously, use `addPlatform()`
instead of `setPlatform()`:
```java
migration.addPlatform(Platform.POSTGRES);
migration.addPlatform(Platform.SQLSERVER17);
migration.addPlatform(Platform.MYSQL);
```
Each platform gets its own subdirectory under `dbmigration/`.
### Include index
When enabled the migration generation also generates a file that contains
all the migrations and their associated hashes. This is a performance
optimisation (that will become the default) and means that the migration
runner just needs to read the one resource and has the pre-computed hash
values (so does not need to read each migration resource and compute the
hash for each of those at runtime).
```java
migration.setIncludeIndex(true);
```
### Strict mode
Strict mode (on by default) errors if there are any pending drops not yet applied.
Set to `false` to allow generation to proceed regardless:
```java
migration.setStrictMode(false);
```
### Applying pending drops
Destructive changes (drop column, drop table) are **not** included in the apply
SQL by default — they are recorded as `pendingDrops` in the model XML. This allows
the application to be deployed without immediately dropping columns (important for
rolling deployments).
The migration runner logs a message when pending drops exist:
```
INFO DbMigration - Pending un-applied drops in versions [1.1]
```
When ready to apply the drops, set `setGeneratePendingDrop` to the version that
contains the pending drops:
```java
migration.setVersion("1.3");
migration.setName("drop-pending-from-1.1");
migration.setGeneratePendingDrop("1.1"); // apply drops recorded in version 1.1
migration.generateMigration();
```
### Custom dbSchema
If the project uses a named Postgres schema (set via `ebean.dbSchema` in
`application.properties`), no additional configuration is needed in
`GenerateDbMigration` — Ebean picks up the schema from the application config
automatically when running in offline mode.
```properties
# application.properties
ebean.dbSchema=myschema
```
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| `no changes detected - no migration written` | Entity beans unchanged since last migration | Make entity bean changes first, then re-run |
| `DbMigration - Pending un-applied drops` | A previous migration has drops not yet applied | Either suppress with `setStrictMode(false)` or apply drops with `setGeneratePendingDrop(...)` |
| Generated SQL is empty or wrong | Wrong working directory path | Set `setPathToResources(...)` to the correct module-relative path |
| `ClassNotFoundException` for entity classes | Test classpath not including main classes | Ensure `exec.classpathScope=test` or run via IDE with test classpath |
| Migrations not running on startup | Property key wrong or `ebean-migration` missing | Verify `ebean[.name].migration.run=true` and that `ebean-migration` is on the classpath |
File diff suppressed because it is too large Load Diff
-492
View File
@@ -1,492 +0,0 @@
# Ebean ORM Bundle — Querying (Flattened)
> Flattened bundle. Content from source markdown guides is inlined below.
---
## Source: `writing-ebean-query-beans.md`
# 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 |
| 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.
---
## 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();
```
### 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
---
## 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.
---
## 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.
---
## 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)
- [Ebean query docs](https://ebean.io/docs/query/)
-787
View File
@@ -1,787 +0,0 @@
# Ebean ORM Bundle — Setup (Flattened)
> Flattened bundle. Content from source markdown guides is inlined below.
---
## Source: `add-ebean-postgres-maven-pom.md`
# 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 1 — Define the Ebean version property
Open the module's `pom.xml` (the one that will use Ebean directly, i.e. the module
containing the database configuration and entity classes).
Inside the `<properties>` block, add the `ebean.version` property if it does not
already exist:
```xml
<properties>
<!-- add this line; use latest stable from https://github.com/ebean-orm/ebean/releases -->
<ebean.version>17.2.0</ebean.version>
</properties>
```
> If the project has a parent POM that already defines `ebean.version`, skip this step.
---
## Step 2 — Add the PostgreSQL JDBC driver dependency
Inside the `<dependencies>` block, add the PostgreSQL JDBC driver:
```xml
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.8</version>
</dependency>
```
> Check [Maven Central](https://central.sonatype.com/artifact/org.postgresql/postgresql)
> for the latest version. If the parent POM manages the PostgreSQL version, omit the
> `<version>` tag.
---
## Step 3 — Add the Ebean PostgreSQL platform dependency
Inside the `<dependencies>` block, add the Ebean Postgres platform dependency:
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-postgres</artifactId>
<version>${ebean.version}</version>
</dependency>
```
This single artifact pulls in the Ebean core, the datasource connection pool
(`ebean-datasource`), and all Postgres-specific support.
---
## Step 4 — Add the ebean-test dependency (test scope)
`ebean-test` configures Ebean for tests and enables automatic Docker container management
for Postgres test instances:
```xml
<!-- test dependencies -->
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
```
---
## Step 5 — Add the ebean-maven-plugin (bytecode enhancement)
Ebean requires bytecode enhancement to provide dirty-checking and lazy-loading.
The `ebean-maven-plugin` performs this enhancement at build time.
Inside the `<build><plugins>` block, add:
```xml
<plugin> <!-- perform ebean enhancement -->
<groupId>io.ebean</groupId>
<artifactId>ebean-maven-plugin</artifactId>
<version>${ebean.version}</version>
<extensions>true</extensions>
</plugin>
```
---
## Step 6 — Add the querybean-generator annotation processor
The `querybean-generator` annotation processor generates type-safe query bean classes
at compile time. It must be registered as an `annotationProcessorPath` inside
`maven-compiler-plugin`.
### Case A — No existing `maven-compiler-plugin` configuration
Add the full plugin entry to `<build><plugins>`:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<annotationProcessorPaths>
<path> <!-- generate ebean query beans -->
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>${ebean.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
```
### Case B — `maven-compiler-plugin` already exists with `<annotationProcessorPaths>`
Locate the existing `<annotationProcessorPaths>` block inside the existing
`maven-compiler-plugin` entry and add the new `<path>` inside it. Do **not** add a
second `<configuration>` block or a second `<annotationProcessorPaths>` block.
Example — if the existing block already has a path for, say, `avaje-nima-generator`:
```xml
<annotationProcessorPaths>
<path>
<groupId>io.avaje</groupId>
<artifactId>avaje-nima-generator</artifactId>
<version>${avaje-nima.version}</version>
</path>
<!-- ADD the new path here, inside the existing block -->
<path>
<groupId>io.ebean</groupId>
<artifactId>querybean-generator</artifactId>
<version>${ebean.version}</version>
</path>
</annotationProcessorPaths>
```
---
## Verification
Run the following to confirm the POM is valid and the project compiles:
```bash
mvn compile -pl <your-module-name>
```
Expected result: `BUILD SUCCESS` with no errors from Ebean or the annotation processor.
---
## Next Step
Proceed to **Step 2: 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.
---
## Source: `add-ebean-postgres-test-container.md`
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 3: Test Container Setup
## Purpose
This guide provides step-by-step instructions for setting up a PostgreSQL Docker
container for tests using `ebean-test-containers`, exposing an `io.ebean.Database`
bean via an Avaje Inject `@TestScope @Factory` class. This is Step 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.
Two variants are covered:
- **Variant A** — plain PostgreSQL
- **Variant B** — PostgreSQL with PostGIS extension
---
## Prerequisites
- **Step 1 complete**: `pom.xml` includes `ebean-postgres`, `ebean-maven-plugin`,
`querybean-generator`, and **`ebean-test`** as a test-scoped dependency
(see `add-ebean-postgres-maven-pom.md`)
- **Avaje Inject** is on the classpath with test support (`io.avaje:avaje-inject-test`)
- **Docker** is installed and running on the developer machine
---
## Overview: Declarative vs Programmatic approach
`ebean-test` supports two ways to configure the test database:
| Approach | How | Best for |
|----------|-----|---------|
| **Declarative** | `src/test/resources/application-test.yaml` | Simple projects with no DI, no image mirrors |
| **Programmatic** | `@TestScope @Factory` class | Avaje Inject tests, private image mirrors (ECR), more control |
This guide uses the **programmatic approach** because it integrates naturally with
Avaje Inject, allows a private mirror to be specified (useful in CI with ECR or similar),
and makes the `Database` injectable into tests.
---
## Step 1 — Verify ebean-test is a test dependency
Confirm the following is present in `pom.xml` (added in Step 1):
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
```
`ebean-test` transitively brings in `ebean-test-containers` which provides
`PostgresContainer` and `PostgisContainer`.
---
## Step 2 — Create a `@TestScope @Factory` class
Create a new class in the test source tree (e.g., `src/test/java/.../testconfig/TestConfiguration.java`).
Annotate it with `@TestScope` and `@Factory` so Avaje Inject uses it only in tests.
```java
package com.example.testconfig;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.avaje.inject.test.TestScope;
import io.ebean.Database;
@TestScope
@Factory
class TestConfiguration {
// bean methods added in the steps below
}
```
---
## Step 3 — Add a container bean and a Database bean
### Variant A — Plain PostgreSQL
```java
import io.ebean.test.containers.PostgresContainer;
@TestScope
@Factory
class TestConfiguration {
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("17") // Postgres image version
.dbName("my_app") // database to create inside the container
.build()
.start();
}
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.build();
}
}
```
### Variant B — PostGIS (PostgreSQL + PostGIS extension)
Use `PostgisContainer` instead of `PostgresContainer`. The default image is
`ghcr.io/baosystems/postgis:{version}` and the extensions `hstore`, `pgcrypto`,
and `postgis` are installed automatically.
```java
import io.ebean.test.containers.PostgisContainer;
@TestScope
@Factory
class TestConfiguration {
@Bean
PostgisContainer postgres() {
return PostgisContainer.builder("17") // PostGIS image version (Postgres 17)
.dbName("my_app")
.build()
.start();
}
@Bean
Database database(PostgisContainer container) {
return container.ebean()
.builder()
.build();
}
}
```
### Key differences from Variant A
| | PostgresContainer | PostgisContainer |
|---|---|---|
| Docker image | `postgres:{version}` | `ghcr.io/baosystems/postgis:{version}` |
| Default extensions | `hstore, pgcrypto` | `hstore, pgcrypto, postgis` |
| Default port | 6432 | 6432 |
| Optional LW mode | — | `.useLW(true)` (see Optional section) |
---
## Step 4 — Write a test
Annotate the test class with `@InjectTest` and inject `Database` with `@Inject`:
```java
package com.example.testconfig;
import io.avaje.inject.test.InjectTest;
import io.ebean.Database;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@InjectTest
class DatabaseTest {
@Inject
Database database;
@Test
void database_isAvailable() {
assertThat(database).isNotNull();
}
}
```
---
## Verification
Run the tests:
```bash
mvn verify
```
Expected log output confirming the container started and Ebean connected:
```
INFO Container ut_postgres running with port:6432 ...
INFO connectivity confirmed for ut_postgres
INFO DataSourcePool [my_app] autoCommit[false] ...
INFO DatabasePlatform name:my_app platform:postgres
INFO Executing db-create-all.sql - ...
```
**Important:** Verify this step passes with `mvn verify` before proceeding to
Step 3 (production database configuration). A working test container gives you
a fast feedback loop for all subsequent entity and query changes.
---
## 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.
---
## Source: `add-ebean-postgres-database-config.md`
# Guide: Add Ebean ORM (PostgreSQL) to an Existing Maven Project — Step 2: Database Configuration
## Purpose
This guide provides step-by-step instructions for configuring an Ebean `Database` bean
using **Avaje Inject** (`@Factory` / `@Bean`), backed by a PostgreSQL datasource built
with Ebean's `DataSourceBuilder`. Follow every step in order. This is Step 3 of 3.
---
## Prerequisites
- **Step 1 complete**: `pom.xml` already includes `ebean-postgres`, `ebean-maven-plugin`,
and `querybean-generator` (see `add-ebean-postgres-maven-pom.md`)
- **Step 2 complete**: Test container setup is working and `mvn verify` passes
(see `add-ebean-postgres-test-container.md`)
- **Avaje Inject** is on the classpath (e.g. `io.avaje:avaje-inject`)
- A configuration source is available at runtime (e.g. `avaje-config` reading
`application.yml` or environment variables)
- The following configuration keys are resolvable at runtime (adapt names to your project):
| Key | Description |
|-----|-------------|
| `db_url` | JDBC URL for the master/write connection |
| `db_user` | Database username |
| `db_pass` | Database password |
| `db_master_min_connections` | Minimum pool size (default: 1) |
| `db_master_initial_connections` | Initial pool size at startup — set high to pre-warm on pod start (see K8s note below) |
| `db_master_max_connections` | Maximum pool size (default: 200) |
---
## Step 1 — Locate or create the `@Factory` class
Look for an existing Avaje Inject `@Factory`-annotated class in the project
(often named `AppConfig`, `DatabaseConfig`, or similar). If one exists, add the new
`@Bean` method to it. If none exists, create one:
```java
package com.example.configuration;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
@Factory
class DatabaseConfig {
// beans will be added in the steps below
}
```
---
## Step 2 — Add the `Database` bean method (minimal — master datasource only)
Add the following `@Bean` method to the `@Factory` class. This creates an Ebean
`Database` backed by a single master (read-write) PostgreSQL datasource.
```java
import io.ebean.Database;
import io.ebean.datasource.DataSourceBuilder;
@Bean
Database database() {
var dataSource = DataSourceBuilder.create()
.url(/* resolve from config, e.g.: */ Config.get("db_url"))
.username(Config.get("db_user"))
.password(Config.get("db_pass"))
.driver("org.postgresql.Driver")
.schema("myschema") // set to your target schema
.applicationName("my-app") // visible in pg_stat_activity
.minConnections(Config.getInt("db_master_min_connections", 1))
.initialConnections(Config.getInt("db_master_initial_connections", 10))
.maxConnections(Config.getInt("db_master_max_connections", 200));
return Database.builder()
.name("db") // logical name for this Database instance
.dataSourceBuilder(dataSource)
.build();
}
```
### Field guidance
| Field | Notes |
|-------|-------|
| `url` | Full JDBC URL, e.g. `jdbc:postgresql://host:5432/dbname` |
| `schema` | The Postgres schema Ebean should use (omit if using `public`) |
| `applicationName` | Shown in `pg_stat_activity.application_name`; helps with DB-side diagnostics |
| `name("db")` | Logical Ebean database name; relevant if multiple Database instances exist |
| `minConnections` | Connections kept open at all times; pool will not shrink below this |
| `initialConnections` | Connections opened at startup; see K8s warm-up note below |
| `maxConnections` | Hard upper limit on concurrent connections |
### Connection pool sizing for Kubernetes (and similar orchestrated environments)
When a pod starts in Kubernetes it will receive live traffic as soon as it passes
readiness checks — often before the connection pool has had a chance to grow to handle
the load. This can cause latency spikes on the first wave of requests while the pool
expands one connection at a time.
Use `initialConnections` to **pre-warm the pool at startup** so it is already sized
for peak load when the pod goes live:
```
minConnections: 2 ← floor; pool will shrink back here when idle
initialConnections: 20 ← opened at pod start, before first request arrives
maxConnections: 50 ← hard ceiling
```
The lifecycle is:
1. **Pod starts** — pool opens `initialConnections` connections immediately.
2. **Pod receives traffic** — pool is already at capacity; no growth latency.
3. **Traffic drops** — idle connections are closed; pool trims back toward `minConnections`.
4. **Next traffic spike** — pool grows again up to `maxConnections` on demand.
Set `initialConnections` to a value high enough that the pool does not need to grow
during the first minute of live traffic. A common starting point is 50–75% of
`maxConnections`.
---
## Step 3 — Inject configuration via a constructor or config helper (recommended)
Rather than calling `Config.get(...)` inline, inject a typed config helper or the
Avaje `Configuration` bean if one is available. This makes the factory testable and
keeps the wiring explicit. For example:
```java
@Bean
Database database(Configuration config) {
String url = config.get("db_url");
String user = config.get("db_user");
String pass = config.get("db_pass");
int min = config.getInt("db_master_min_connections", 1);
int init = config.getInt("db_master_initial_connections", 10);
int max = config.getInt("db_master_max_connections", 200);
var dataSource = DataSourceBuilder.create()
.url(url)
.username(user)
.password(pass)
.driver("org.postgresql.Driver")
.schema("myschema")
.applicationName("my-app")
.minConnections(min)
.initialConnections(init)
.maxConnections(max);
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.skipDataSourceCheck(true)
.build();
}
```
If the project has a dedicated config-wrapper class (a `@Component` that reads config
keys), accept it as a parameter instead of `Configuration`.
---
## Step 4 (Optional) — Add a read-only datasource
For production services that have a separate read-replica, add a second
`DataSourceBuilder` for read-only queries and wire it via
`readOnlyDataSourceBuilder(...)`. The read-only datasource:
- Uses `readOnly(true)` and `autoCommit(true)` (Ebean routes read queries there automatically)
- Typically has a higher max connection count than the master
- Benefits from a prepared-statement cache (`pstmtCacheSize`)
```java
@Bean
Database database(Configuration config) {
String masterUrl = config.get("db_url");
String readOnlyUrl = config.get("db_url_readonly");
String user = config.get("db_user");
String pass = config.get("db_pass");
var masterDataSource = buildDataSource(user, pass)
.url(masterUrl)
.minConnections(config.getInt("db_master_min_connections", 1))
.initialConnections(config.getInt("db_master_initial_connections", 10))
.maxConnections(config.getInt("db_master_max_connections", 50));
var readOnlyDataSource = buildDataSource(user, pass)
.url(readOnlyUrl)
.readOnly(true)
.autoCommit(true)
.pstmtCacheSize(250) // cache up to 250 prepared statements per connection
.maxInactiveTimeSecs(600) // close idle connections after 10 minutes
.minConnections(config.getInt("db_readonly_min_connections", 2))
.initialConnections(config.getInt("db_readonly_initial_connections", 10))
.maxConnections(config.getInt("db_readonly_max_connections", 200));
return Database.builder()
.name("db")
.dataSourceBuilder(masterDataSource)
.readOnlyDataSourceBuilder(readOnlyDataSource)
.build();
}
private static DataSourceBuilder buildDataSource(String user, String pass) {
return DataSourceBuilder.create()
.username(user)
.password(pass)
.driver("org.postgresql.Driver")
.schema("myschema")
.applicationName("my-app")
.addProperty("prepareThreshold", "2"); // PostgreSQL: server-side prepared statements
}
```
### Additional configuration keys for the read-only datasource
| Key | Description | Default |
|-----|-------------|---------|
| `db_url_readonly` | JDBC URL for the read replica | — |
| `db_master_initial_connections` | Initial master pool size at startup | 10 |
| `db_readonly_min_connections` | Minimum pool size | 2 |
| `db_readonly_initial_connections` | Initial pool size at startup | same as min |
| `db_readonly_max_connections` | Maximum pool size | 20 |
---
## Step 5 (Optional) — Enable the migration runner
If the project uses Ebean's built-in DB migration runner to apply SQL migrations on
startup, enable it on the `DatabaseBuilder`:
```java
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.runMigration(true) // run pending migrations on startup
.build();
```
This is equivalent to setting `ebean.migration.run=true` in `application.properties`
but is preferred because it keeps all database configuration in one place. To make it
conditional (e.g. only in non-production environments):
```java
.runMigration(config.getBoolean("db.runMigrations", false))
```
See the DB migration generation guide (`add-ebean-db-migration-generation.md`) for
full details on generating and managing migration files.
---
## See Also
For advanced connection pool configuration, production deployment patterns, and connection
validation best practices, see the [ebean-datasource guides](https://github.com/ebean-orm/ebean-datasource/tree/master/docs/guides/):
- **[Creating DataSource Pools](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/create-datasource-pool.md)** — Covers read-only pools (`readOnly(true)` + `autoCommit(true)`), Kubernetes deployment strategies using `initialConnections`, and AWS Lambda optimization
- **[AWS Aurora Read-Write Split](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/aws-aurora-read-write-split.md)** — Setting up dual DataSources with Aurora reader and writer endpoints, including Ebean secondary datasource routing
- **[Connection Validation Best Practices](https://github.com/ebean-orm/ebean-datasource/blob/master/docs/guides/connection-validation-best-practices.md)** — Why `Connection.isValid()` is the recommended default and when (rarely) explicit `heartbeatSql` is needed
---
## Verification
1. Start the application (or run `mvn test -pl <your-module>`).
2. Look for log output similar to:
```
INFO o.a.datasource.pool.ConnectionPool - DataSourcePool [db] autoCommit[false] min[1] max[5]
INFO io.ebean.internal.DefaultContainer - DatabasePlatform name:db platform:postgres
```
3. If you see `DataSourcePool` and `DatabasePlatform` log lines, Ebean is connected and
the database bean is wired correctly.
---
## Troubleshooting
| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| `ClassNotFoundException: org.postgresql.Driver` | PostgreSQL JDBC driver missing | Add `org.postgresql:postgresql` dependency (see Step 1 guide) |
| `Cannot connect to database` at startup | DB unreachable but `skipDataSourceCheck` is `false` | Set `.skipDataSourceCheck(true)` |
| Ebean enhancement warnings in logs | `ebean-maven-plugin` not configured | Complete Step 1 guide |
| `NullPointerException` reading config key | Config key not defined | Add the key to `application.yml` or environment |
---
## Related
The test container setup (Step 2) should already be complete and passing
before this step. See `add-ebean-postgres-test-container.md`.
-825
View File
@@ -1,825 +0,0 @@
# Ebean ORM Bundle — Testing (Flattened)
> Flattened bundle. Content from source markdown guides is inlined below.
---
## Source: `testing-with-testentitybuilder.md`
# Guide: Testing with TestEntityBuilder
## Purpose
This guide explains how to use `TestEntityBuilder` to rapidly create test entity instances with auto-populated random values. It is written as practical instructions for developers and AI agents building tests for Ebean applications.
`TestEntityBuilder` eliminates boilerplate test setup by automatically generating realistic test data for all scalar fields, while respecting entity constraints and relationships. This is particularly valuable for:
- **Integration tests** that need representative data without caring about specific values
- **Persistence layer tests** that verify save/update/delete operations work correctly
- **Query and filter tests** where you need multiple entities with varied data
- **Rapid test setup** that reduces test code verbosity and improves readability
---
## Setup & Dependencies
### Add ebean-test to Your Project
The `TestEntityBuilder` class is provided by the `ebean-test` module.
**Maven:**
```xml
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
```
**Gradle:**
```gradle
testImplementation "io.ebean:ebean-test:${ebeanVersion}"
```
Use a version that matches your Ebean runtime (`ebean.version` /
`ebeanVersion`), or replace with an explicit fixed version if your build does
not centralize dependency versions.
> **Minimum version:** `TestEntityBuilder` was introduced in `ebean-test 17.5.0`. If your
> existing Ebean version is below this, upgrade before proceeding — mismatched Ebean
> runtime and test versions are not supported.
### Import the Class
```java
import io.ebean.test.TestEntityBuilder;
```
---
## Basic Usage
### Create a Builder Instance
`TestEntityBuilder` uses a builder pattern for configuration:
```java
TestEntityBuilder builder = TestEntityBuilder.builder(database).build();
```
The `Database` parameter specifies which Ebean database instance to use for entity type
lookups and persistence operations. Pass the injected `Database` bean (see
[Using with Dependency Injection](#using-with-dependency-injection) below) rather than
`DB.getDefault()` when working in a Spring or Avaje Inject context. For the same reason,
use the injected `database` bean for **all** persistence operations in your tests
(`database.save()`, `database.find()`, etc.) rather than mixing in static `DB.*` calls.
### Build an Entity (In-Memory)
The `build()` method creates an instance with populated fields **without persisting to the database:**
```java
Product product = builder.build(Product.class);
// Fields are populated:
// - id: unset (typically 0 for primitive long, null for boxed Long)
// - name: random UUID-based string
// - price: random BigDecimal
// - inStock: true
// - createdAt: current instant
// - etc.
// Not persisted yet (`@Id` is still unset until the entity is persisted).
```
### Build and Save (Persist to Database)
The `save()` method creates, persists, and returns an entity with the database-assigned `@Id`:
```java
Product product = builder.save(Product.class);
// Entity is now in the database:
assert database.find(Product.class, product.getId()) != null;
```
### Save Multiple Entities
The `saveAll()` method persists multiple pre-built entities in a single call:
```java
Product p1 = builder.build(Product.class);
Product p2 = builder.build(Product.class);
builder.saveAll(p1, p2);
// Both are now in the database with assigned IDs:
assert p1.getId() != null;
assert p2.getId() != null;
```
This is equivalent to `database.saveAll(p1, p2)` but avoids needing a separate
`Database` reference in tests that already hold a `TestEntityBuilder`.
### Access the Underlying Database
The `database()` method returns the `Database` instance used internally by the builder.
This is useful in tests where you want a single injected object (`TestEntityBuilder`) but
still need to perform `find()`, `delete()`, or other database operations:
```java
Product saved = builder.save(Product.class);
// Use builder.database() instead of injecting a separate Database bean:
Product found = builder.database().find(Product.class, saved.getId());
assert found != null;
```
---
## Using with Dependency Injection
Most applications using Ebean also use a DI framework. The recommended pattern is to
register `TestEntityBuilder` as a bean in the test DI context so it can be injected
directly into test classes — eliminating `@BeforeEach` setup boilerplate entirely.
### Avaje Inject — `@TestScope @Factory`
Add a `@Bean` method to your test-scoped `@Factory` class:
```java
import io.ebean.Database;
import io.ebean.test.ContainerDatabase;
import io.avaje.inject.Bean;
import io.avaje.inject.Factory;
import io.avaje.inject.test.TestScope;
import io.ebean.test.TestEntityBuilder;
@TestScope
@Factory
class TestConfiguration {
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("17") // Postgres image version
.dbName("my_app") // database to create inside the container
.build()
.start();
}
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.build();
}
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
```
Then inject it directly into test classes using `@InjectTest`:
```java
@InjectTest
class OrderControllerTest {
@Inject Database database;
@Inject TestEntityBuilder builder;
@Test
void findByStatus() {
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
database.save(order);
// ... test assertions
}
}
```
Both patterns produce a single shared `TestEntityBuilder` instance, wired
from the managed `Database` bean — no `@BeforeEach` required.
### Spring Boot — `@TestConfiguration`
Add a `@TestConfiguration` class that provides `TestEntityBuilder` as a bean:
```java
@TestConfiguration
class TestConfig {
@Bean
PostgresContainer postgres() {
return PostgresContainer.builder("17") // Postgres image version
.dbName("my_app") // database to create inside the container
.build()
.start();
}
// use @Primary if your main application context also wires a Database bean
// or conditionally wire the main Database bean to exclude it from tests
@Primary
@Bean
Database database(PostgresContainer container) {
return container.ebean()
.builder()
.build();
}
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
```
Then inject it directly into test classes:
```java
@SpringBootTest
class OrderControllerTest {
@Autowired Database database;
@Autowired TestEntityBuilder builder;
@Test
void findByStatus() {
var order = builder.build(Order.class).setStatus(OrderStatus.PENDING);
database.save(order);
// ... test assertions
}
}
```
---
## Type-Specific Value Generation
`TestEntityBuilder` generates appropriate random values for each Java/SQL type. Customize this behavior by subclassing `RandomValueGenerator` (see "Custom Value Generators" below).
| Type | Generated Value | Notes |
|------|-----------------|-------|
| `String` | UUID-derived (8 chars by default) | Truncated to column length if `@Column(length=...)` is set |
| Email fields | `uuid@domain.com` format | Detected when property name contains "email" (case-insensitive) |
| `Integer` / `int` | Random in `[1, 1_000)` | |
| `Long` / `long` | Random in `[1, 100_000)` | |
| `Short` / `short` | Random in `[1, 100)` | See note on flag fields below |
| `Double` / `double` | Random in `[1, 100)` | |
| `Float` / `float` | Random in `[1, 100)` | |
| `BigDecimal` | Respects precision and scale | Precision and scale from `@Column(precision=..., scale=...)` |
| `Boolean` / `boolean` | `true` | Override in custom generator if needed |
| `UUID` | Random UUID | Via `UUID.randomUUID()` |
| `LocalDate` | Today's date | Via `LocalDate.now()` |
| `LocalDateTime` | Current datetime | Via `LocalDateTime.now()` |
| `Instant` | Current instant | Via `Instant.now()` |
| `OffsetDateTime` | Current time with zone | Via `OffsetDateTime.now()` |
| `ZonedDateTime` | Current time with zone | Via `ZonedDateTime.now()` |
| `Enum` | First constant | Override in custom generator if needed |
| Other types | `null` | Set these fields manually in tests |
### String Length Constraints
`TestEntityBuilder` respects column length constraints defined in the entity:
```java
@Entity
public class User {
@Column(length = 50)
private String username;
}
User user = builder.build(User.class);
assert user.getUsername().length() <= 50; // ✅ Constraint respected
```
### BigDecimal Precision and Scale
For `BigDecimal` fields, the builder respects the database column precision and scale:
```java
@Entity
public class LineItem {
@Column(precision = 10, scale = 2) // max 99_999_999.99
private BigDecimal amount;
}
LineItem item = builder.build(LineItem.class);
assert item.getAmount().scale() == 2;
```
### Short Fields Used as Boolean Flags
Some legacy schemas use `short` to represent boolean-like flags (e.g. `active = 1`
means active, `0` means inactive). `TestEntityBuilder` generates a random short in
`[1, 100)`, which will be non-zero but not necessarily `1`. If your application
code checks `entity.getActive() == 1` specifically, override the field after building:
```java
Organisation org = builder.build(Organisation.class)
.setActive((short) 1); // explicit override — random short won't do
```
---
## Entity Relationships
### Cascade-Persist Relationships: Recursively Built
Relationships marked with `cascade = PERSIST` are recursively populated:
```java
@Entity
public class Order {
@ManyToOne(cascade = CascadeType.PERSIST)
private Customer customer;
}
Order order = builder.build(Order.class);
// Both order and customer are built:
assert order != null;
assert order.getCustomer() != null;
// Before persist, @Id values are typically unset
// (0 for primitive IDs, null for boxed IDs).
// When saved, cascade handles both:
Order saved = builder.save(Order.class);
assert saved.getId() != null;
assert saved.getCustomer().getId() != null; // parent also saved
```
### Non-Cascade Relationships: Left Null
Relationships without cascade persist are not auto-created — even if marked `optional = false`.
Create and save the related entity first (the builder works well here), then assign it manually
before saving the parent:
```java
@Entity
public class BlogPost {
@ManyToOne
private Author author; // No cascade = left null by builder
}
BlogPost post = builder.build(BlogPost.class);
assert post.getAuthor() == null;
// Use the builder to create the related entity, then set it manually:
Author author = builder.save(Author.class);
post.setAuthor(author);
database.save(post);
```
### Collection Relationships: Left Empty
Collection relationships (`@OneToMany`, `@ManyToMany`) are left empty. On Ebean-enhanced
entities these fields are initialised to empty Ebean-managed lists (not `null`), so calling
`.add()` or `.addAll()` directly is safe:
```java
@Entity
public class Author {
@OneToMany(mappedBy = "author")
private List<BlogPost> posts; // Left empty
}
Author author = builder.build(Author.class);
assert author.getPosts().isEmpty();
// Populate if needed for testing:
author.getPosts().addAll(Arrays.asList(post1, post2, post3));
```
### Cycle Detection: Prevents Infinite Recursion
If two entities reference each other with cascade persist, the builder detects the cycle and breaks it by leaving one reference null:
```java
@Entity
public class Person {
@ManyToOne(cascade = CascadeType.PERSIST)
private Organization org;
}
@Entity
public class Organization {
@ManyToOne(cascade = CascadeType.PERSIST)
private Person founder;
}
Person person = builder.build(Person.class);
// One reference will be null to break the cycle:
// either person.org or person.org.founder is null
```
---
## Custom Value Generators
### Why Customize?
The default `RandomValueGenerator` uses generic random values. For domain-specific testing, you may want:
- Email addresses with your company domain
- Realistic phone numbers
- Product SKUs following a pattern
- Addresses in specific regions
- Monetary amounts within realistic ranges
### Creating a Custom Generator
Subclass `RandomValueGenerator` and override individual `random*()` methods:
```java
class CompanyTestDataGenerator extends RandomValueGenerator {
@Override
protected String randomString(String propName, int maxLength) {
if (propName != null && propName.toLowerCase().contains("email")) {
// Use company domain instead of generic @domain.com
String localPart = UUID.randomUUID().toString().substring(0, 8);
String email = localPart + "@mycompany.com";
if (maxLength > 0 && email.length() > maxLength) {
return email.substring(0, maxLength);
}
return email;
}
return super.randomString(propName, maxLength);
}
// Override other methods as needed:
@Override
protected Object randomEnum(Class<?> type) {
if (type == OrderStatus.class) {
// Bias towards common statuses for realistic test data
return ThreadLocalRandom.current().nextDouble() < 0.8
? OrderStatus.PENDING
: OrderStatus.COMPLETED;
}
return super.randomEnum(type);
}
}
```
### Using a Custom Generator
Pass the custom generator when building:
```java
TestEntityBuilder builder = TestEntityBuilder.builder(database)
.valueGenerator(new CompanyTestDataGenerator())
.build();
User user = builder.build(User.class);
assert user.getEmail().endsWith("@mycompany.com");
```
In a DI context, register this as the bean:
```java
// Spring Boot
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database)
.valueGenerator(new CompanyTestDataGenerator())
.build();
}
```
### Example: Money Type
```java
public class MoneyValueGenerator extends RandomValueGenerator {
@Override
protected BigDecimal randomBigDecimal(int precision, int scale) {
// Generate prices in a realistic range: $5.00 to $999.99
BigDecimal price = BigDecimal.valueOf(
ThreadLocalRandom.current().nextDouble(5.0, 1000.0)
);
return price.setScale(2, RoundingMode.HALF_UP);
}
}
```
---
## Best Practices
### 1. Use for Integration Tests, Not Unit Tests
✅ **Good:** Integration test with database
```java
@Test
void whenSaving_thenCanRetrieve() {
Product product = builder.save(Product.class);
Product found = database.find(Product.class, product.getId());
assertThat(found).isNotNull();
}
```
❌ **Poor:** Validation test requiring specific values
```java
@Test
void whenNameIsBlank_thenThrowException() {
Product product = builder.build(Product.class); // name is random!
product.setName(""); // have to override anyway
// ... test proceeds
}
```
### 2. Override Values for Specific Test Scenarios
When test requirements demand specific field values, manually override after building:
```java
@Test
void whenStockIsLow_thenShowWarning() {
Product product = builder.build(Product.class);
product.setQuantity(2); // Specific value for this test
boolean shouldWarn = product.shouldShowLowStockWarning();
assertThat(shouldWarn).isTrue();
}
```
### 3. Create Fixture Factories for Common Patterns
For shared domain-specific setup, encapsulate build patterns in an instance helper class
rather than a static factory. In a DI context, this class can be registered as a bean
alongside `TestEntityBuilder`:
```java
// Spring Boot
@TestConfiguration
class TestConfig {
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
@Bean
OrderTestFactory orderTestFactory(TestEntityBuilder builder, Database database) {
return new OrderTestFactory(builder, database);
}
}
public class OrderTestFactory {
private final TestEntityBuilder builder;
private final Database database;
public OrderTestFactory(TestEntityBuilder builder, Database database) {
this.builder = builder;
this.database = database;
}
public Order savePendingOrder() {
Order order = builder.build(Order.class);
order.setStatus(OrderStatus.PENDING);
database.save(order);
return order;
}
public Order saveShippedOrder() {
Order order = builder.build(Order.class);
order.setStatus(OrderStatus.SHIPPED);
order.setShippedAt(Instant.now());
database.save(order);
return order;
}
}
// Usage in tests:
@SpringBootTest
class OrderControllerTest {
@Autowired OrderTestFactory orderFactory;
@Test
void whenOrderPending_thenCanUpdate() {
Order order = orderFactory.savePendingOrder();
// ... test logic
}
}
```
### 4. Build Multiple Distinct Instances
Each call to `build()` or `save()` produces a new instance with fresh random values:
```java
@Test
void whenFetchingMultipleOrders_thenAllUnique() {
Order order1 = builder.save(Order.class);
Order order2 = builder.save(Order.class);
Order order3 = builder.save(Order.class);
assertThat(order1.getId()).isNotEqualTo(order2.getId());
assertThat(order2.getId()).isNotEqualTo(order3.getId());
assertThat(order1.getOrderNumber()).isNotEqualTo(order2.getOrderNumber());
}
```
---
## Complete Examples
### Example 1: Integration Test with Spring Boot
Register `TestEntityBuilder` as a `@TestConfiguration` bean, then inject it alongside
the repository under test:
```java
@TestConfiguration
class TestConfig {
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
@SpringBootTest
class OrderRepositoryTest {
@Autowired OrderRepository orderRepository;
@Autowired TestEntityBuilder builder;
@Test
void whenFindingOrdersByStatus_thenReturnsMatching() {
Order pending1 = builder.build(Order.class);
pending1.setStatus(OrderStatus.PENDING);
Order pending2 = builder.build(Order.class);
pending2.setStatus(OrderStatus.PENDING);
Order shipped = builder.build(Order.class);
shipped.setStatus(OrderStatus.SHIPPED);
builder.saveAll(pending1, pending2, shipped);
List<Order> pending = orderRepository.findByStatus(OrderStatus.PENDING);
assertThat(pending).hasSize(2);
}
}
```
### Example 2: Integration Test with Avaje Inject
```java
@TestScope
@Factory
class TestConfiguration {
@Bean
TestEntityBuilder testEntityBuilder(Database database) {
return TestEntityBuilder.builder(database).build();
}
}
@InjectTest
class OrderControllerTest {
@Inject TestEntityBuilder builder;
@Test
void whenFindingOrdersByStatus_thenReturnsMatching() {
Order pending1 = builder.build(Order.class);
pending1.setStatus(OrderStatus.PENDING);
Order pending2 = builder.build(Order.class);
pending2.setStatus(OrderStatus.PENDING);
Order shipped = builder.build(Order.class);
shipped.setStatus(OrderStatus.SHIPPED);
builder.saveAll(pending1, pending2, shipped);
// ... test assertions
}
}
```
### Example 3: Recursive Relationship Building
```java
@Test
void whenBuildingOrderWithCustomer_thenBothPopulated() {
Order order = builder.build(Order.class);
// Customer is recursively built because of @ManyToOne(cascade=PERSIST)
assertThat(order.getCustomer()).isNotNull();
// Before persist, @Id values are typically unset
// (0 for primitive IDs, null for boxed IDs).
assertThat(order.getCustomer().getName()).isNotNull();
// Saving cascades to customer:
Order saved = builder.save(Order.class);
assertThat(saved.getId()).isNotNull();
assertThat(saved.getCustomer().getId()).isNotNull();
}
```
### Example 4: Custom Generator for Domain Values
```java
// Custom generator for your domain
class ECommerceTestDataGenerator extends RandomValueGenerator {
@Override
protected BigDecimal randomBigDecimal(int precision, int scale) {
// Product prices typically range $10-$500
return BigDecimal.valueOf(
ThreadLocalRandom.current().nextDouble(10.0, 500.0)
).setScale(2, RoundingMode.HALF_UP);
}
}
@Test
void usingCustomGenerator() {
TestEntityBuilder builder = TestEntityBuilder.builder(database)
.valueGenerator(new ECommerceTestDataGenerator())
.build();
Product product = builder.build(Product.class);
assertThat(product.getPrice())
.isBetween(BigDecimal.TEN, BigDecimal.valueOf(500.0));
}
```
---
## Troubleshooting
### "No BeanDescriptor found for [Class] — is it an @Entity?"
**Cause:** The class you're trying to build is not registered as an Ebean entity.
**Solution:** Ensure the class is annotated with `@Entity` and registered with the Database:
```java
@Entity
@Table(name = "products")
public class Product {
// ...
}
```
### Fields are unset even though I expected them to be populated
**Cause:** `TestEntityBuilder` does **not** populate:
- `@Id` fields (identity/primary key; left unset until persist)
- `@Version` fields (optimistic locking; left unset until persist)
- `@Transient` fields
- `@OneToMany` collections
- Non-cascade `@ManyToOne` relationships
**Solution:** Set only the fields your test scenario cares about, then persist.
`@Id` and `@Version` are usually database-managed and should typically be left
unset before save:
```java
Product product = builder.build(Product.class);
product.setName("specific-name"); // test-specific override
database.save(product); // database assigns @Id/@Version
```
### Building recursive relationships causes StackOverflowError
**Cause:** Two or more entities mutually reference each other without cycle detection.
**Solution:** This should be handled automatically by cycle detection. If not, manually set one reference to null:
```java
Person person = builder.build(Person.class);
person.getOrganization().setFounder(null); // Break cycle
```
### Values generated are "too random" for my test
**Cause:** Default `RandomValueGenerator` uses true random values, which aren't suitable when your test needs predictable data.
**Solution:** Create a custom generator that produces deterministic values:
```java
class DeterministicTestDataGenerator extends RandomValueGenerator {
private int counter = 0;
@Override
protected String randomString(String propName, int maxLength) {
return "test_" + (counter++);
}
}
```
---
## Summary
`TestEntityBuilder` accelerates test development by:
1. **Reducing boilerplate** — No need to manually set every field
2. **Improving readability** — Tests focus on what matters, not setup
3. **Enabling variety** — Each build produces distinct random values
4. **Respecting constraints** — Column lengths and decimal scales are enforced
5. **Supporting customization** — Extend `RandomValueGenerator` for domain needs
@@ -1,455 +0,0 @@
# Ebean ORM Bundle — Write & Transactions (Flattened)
> Flattened bundle. Content from source markdown guides is inlined below.
---
## Source: `persisting-and-transactions-with-ebean.md`
# 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)
-78
View File
@@ -1,78 +0,0 @@
#!/bin/bash
#
# Generates flattened reference bundles for the ebean-orm skill
# from the source guides in docs/guides/.
#
# Run from the ebean repo root, or the script will locate it automatically.
# Output goes to docs/skill/ebean-orm/references/
#
# Usage:
# ./docs/skill/generate-references.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GUIDES_DIR="$SCRIPT_DIR/../guides"
REFS_DIR="$SCRIPT_DIR/ebean-orm/references"
if [ ! -d "$GUIDES_DIR" ]; then
echo "Error: guides directory not found at $GUIDES_DIR" >&2
exit 1
fi
mkdir -p "$REFS_DIR"
# Generate a flattened bundle from one or more source guides.
# generate_bundle <output-name> <title> <guide1> [guide2] ...
generate_bundle() {
local output_name="$1"
local title="$2"
shift 2
local output_file="$REFS_DIR/${output_name}.md"
{
echo "# Ebean ORM Bundle — ${title} (Flattened)"
echo ""
echo "> Flattened bundle. Content from source markdown guides is inlined below."
for guide in "$@"; do
local guide_file="$GUIDES_DIR/$guide"
if [ ! -f "$guide_file" ]; then
echo "Warning: guide not found: $guide_file" >&2
continue
fi
echo ""
echo "---"
echo ""
echo "## Source: \`${guide}\`"
echo ""
cat "$guide_file"
done
} > "$output_file"
echo " Generated $output_name.md ($(wc -c < "$output_file" | tr -d ' ') bytes)"
}
echo "Generating ebean-orm skill references from docs/guides/ ..."
generate_bundle "setup" "Setup" \
add-ebean-postgres-maven-pom.md \
add-ebean-postgres-test-container.md \
add-ebean-postgres-database-config.md
generate_bundle "modeling" "Modeling" \
entity-bean-creation.md \
lombok-with-ebean-entity-beans.md
generate_bundle "querying" "Querying" \
writing-ebean-query-beans.md
generate_bundle "write-transactions" "Write & Transactions" \
persisting-and-transactions-with-ebean.md
generate_bundle "testing" "Testing" \
testing-with-testentitybuilder.md
generate_bundle "db-migrations" "DB Migrations" \
add-ebean-db-migration-generation.md
echo "Done. References written to docs/skill/ebean-orm/references/"