From 50e20c3aa0cc3c26c9fc40bc29d8af3363be7dd4 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Fri, 1 May 2026 15:38:37 +1200 Subject: [PATCH] Docs: update docs / skills location to separate git repo --- docs/guides/README.md | 17 + docs/guides/add-ebean-postgres-maven-pom.md | 6 +- docs/skill/ebean-orm/SKILL.md | 104 -- .../ebean-orm/references/db-migrations.md | 374 ------ docs/skill/ebean-orm/references/modeling.md | 1016 ----------------- docs/skill/ebean-orm/references/querying.md | 492 -------- docs/skill/ebean-orm/references/setup.md | 787 ------------- docs/skill/ebean-orm/references/testing.md | 825 ------------- .../references/write-transactions.md | 455 -------- docs/skill/generate-references.sh | 78 -- 10 files changed, 21 insertions(+), 4133 deletions(-) delete mode 100644 docs/skill/ebean-orm/SKILL.md delete mode 100644 docs/skill/ebean-orm/references/db-migrations.md delete mode 100644 docs/skill/ebean-orm/references/modeling.md delete mode 100644 docs/skill/ebean-orm/references/querying.md delete mode 100644 docs/skill/ebean-orm/references/setup.md delete mode 100644 docs/skill/ebean-orm/references/testing.md delete mode 100644 docs/skill/ebean-orm/references/write-transactions.md delete mode 100755 docs/skill/generate-references.sh diff --git a/docs/guides/README.md b/docs/guides/README.md index 1322b5de2..e64cd6bd0 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -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. diff --git a/docs/guides/add-ebean-postgres-maven-pom.md b/docs/guides/add-ebean-postgres-maven-pom.md index 0248702a2..488f18385 100644 --- a/docs/guides/add-ebean-postgres-maven-pom.md +++ b/docs/guides/add-ebean-postgres-maven-pom.md @@ -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 +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. --- diff --git a/docs/skill/ebean-orm/SKILL.md b/docs/skill/ebean-orm/SKILL.md deleted file mode 100644 index 78714fce9..000000000 --- a/docs/skill/ebean-orm/SKILL.md +++ /dev/null @@ -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 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 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. diff --git a/docs/skill/ebean-orm/references/db-migrations.md b/docs/skill/ebean-orm/references/db-migrations.md deleted file mode 100644 index ed381c597..000000000 --- a/docs/skill/ebean-orm/references/db-migrations.md +++ /dev/null @@ -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 - - io.ebean - ebean-migration - ${ebean.version} - -``` - ---- - -## 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 -``` - -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 - - - - - - - - -``` - ---- - -## 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 | diff --git a/docs/skill/ebean-orm/references/modeling.md b/docs/skill/ebean-orm/references/modeling.md deleted file mode 100644 index 3c5fd27e1..000000000 --- a/docs/skill/ebean-orm/references/modeling.md +++ /dev/null @@ -1,1016 +0,0 @@ -# Ebean ORM Bundle — Modeling (Flattened) - -> Flattened bundle. Content from source markdown guides is inlined below. - ---- - -## Source: `entity-bean-creation.md` - -# 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 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 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 orders; // ❌ Set calls equals/hashCode before IDs assigned -} -``` - -**DO:** -```java -@Entity -public class Customer { - @Id - private long id; - - @OneToMany(mappedBy = "customer") - private List 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 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/` - ---- - -## Source: `lombok-with-ebean-entity-beans.md` - -# 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`** diff --git a/docs/skill/ebean-orm/references/querying.md b/docs/skill/ebean-orm/references/querying.md deleted file mode 100644 index 8f6b1be34..000000000 --- a/docs/skill/ebean-orm/references/querying.md +++ /dev/null @@ -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 customers = new QCustomer() - .status.equalTo(Customer.Status.ACTIVE) - .name.istartsWith("rob") - .findList(); -``` - -### Example - association traversal - -```java -List customers = new QCustomer() - .billingAddress.city.equalTo("Auckland") - .findList(); -``` - -### Example - collection predicate - -```java -List 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 customers = new QCustomer() - .status.equalTo(Customer.Status.ACTIVE) - .orderBy().name.asc() - .setMaxRows(50) - .findList(); -``` - -### Example - offset/limit pagination - -```java -List customers = new QCustomer() - .status.equalTo(Customer.Status.ACTIVE) - .orderBy().id.asc() - .setFirstRow(offset) - .setMaxRows(pageSize) - .findList(); -``` - -### Example - paged list with total count - -```java -PagedList page = new QCustomer() - .status.equalTo(Customer.Status.ACTIVE) - .orderBy().id.asc() - .setFirstRow(offset) - .setMaxRows(pageSize) - .findPagedList(); - -page.loadRowCount(); -List 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 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 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 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_SUMMARY = - QCustomer.forFetchGroup() - .select(CUST.name, CUST.status, CUST.whenCreated) - .billingAddress.fetch() - .buildFetchGroup(); - -List 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 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 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 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/) diff --git a/docs/skill/ebean-orm/references/setup.md b/docs/skill/ebean-orm/references/setup.md deleted file mode 100644 index 54ae0d943..000000000 --- a/docs/skill/ebean-orm/references/setup.md +++ /dev/null @@ -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 `` block, add the `ebean.version` property if it does not -already exist: - -```xml - - - 17.2.0 - -``` - -> 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 `` block, add the PostgreSQL JDBC driver: - -```xml - - org.postgresql - postgresql - 42.7.8 - -``` - -> 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 -> `` tag. - ---- - -## Step 3 — Add the Ebean PostgreSQL platform dependency - -Inside the `` block, add the Ebean Postgres platform dependency: - -```xml - - io.ebean - ebean-postgres - ${ebean.version} - -``` - -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 - - - io.ebean - ebean-test - ${ebean.version} - test - -``` - ---- - -## 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 `` block, add: - -```xml - - io.ebean - ebean-maven-plugin - ${ebean.version} - true - -``` - ---- - -## 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 ``: - -```xml - - org.apache.maven.plugins - maven-compiler-plugin - 3.15.0 - - - - io.ebean - querybean-generator - ${ebean.version} - - - - -``` - -### Case B — `maven-compiler-plugin` already exists with `` - -Locate the existing `` block inside the existing -`maven-compiler-plugin` entry and add the new `` inside it. Do **not** add a -second `` block or a second `` block. - -Example — if the existing block already has a path for, say, `avaje-nima-generator`: - -```xml - - - io.avaje - avaje-nima-generator - ${avaje-nima.version} - - - - io.ebean - querybean-generator - ${ebean.version} - - -``` - ---- - -## Verification - -Run the following to confirm the POM is valid and the project compiles: - -```bash -mvn compile -pl -``` - -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 - - io.ebean - ebean-test - ${ebean.version} - test - -``` - -`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 - - - net.postgis - postgis-jdbc - 2024.1.0 - test - -``` - -```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 `). -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`. diff --git a/docs/skill/ebean-orm/references/testing.md b/docs/skill/ebean-orm/references/testing.md deleted file mode 100644 index f3732e6cd..000000000 --- a/docs/skill/ebean-orm/references/testing.md +++ /dev/null @@ -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 - - io.ebean - ebean-test - ${ebean.version} - test - -``` - -**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 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 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 - diff --git a/docs/skill/ebean-orm/references/write-transactions.md b/docs/skill/ebean-orm/references/write-transactions.md deleted file mode 100644 index 770c40f8a..000000000 --- a/docs/skill/ebean-orm/references/write-transactions.md +++ /dev/null @@ -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 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 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) diff --git a/docs/skill/generate-references.sh b/docs/skill/generate-references.sh deleted file mode 100755 index 6bc900f97..000000000 --- a/docs/skill/generate-references.sh +++ /dev/null @@ -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 <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/"