diff --git a/README.md b/README.md
index 5dda8a0ae..6531dd5f7 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,8 @@ or [github discussions](https://github.com/ebean-orm/ebean/discussions)
Goto [https://ebean.io/docs/](https://ebean.io/docs/)
## Guides
+Library reference (capabilities, scope, and AI guidance): [docs/LIBRARY.md](docs/LIBRARY.md)
+
Step-by-step guides for common tasks: [docs/guides/](docs/guides/README.md)
Available guides:
diff --git a/docs/guides/README.md b/docs/guides/README.md
index 116b2923a..9835899cf 100644
--- a/docs/guides/README.md
+++ b/docs/guides/README.md
@@ -20,7 +20,7 @@ existing Maven project. Complete the steps in order.
| Guide | Description |
|-------|-------------|
-| [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; minimal boilerplate without getters/setters; decision tree for code generation |
+| [Entity Bean Creation](entity-bean-creation.md) | How to generate clean, idiomatic Ebean entity beans for AI agents; patterns and anti-patterns; field visibility and accessor guidance; minimal boilerplate |
| [Lombok with Ebean entity beans](lombok-with-ebean-entity-beans.md) | Which Lombok annotations to use and avoid on entity beans; why `@Data` is incompatible with Ebean; how to use `@Getter` + `@Setter` + `@Accessors(chain = true)` |
## Querying
diff --git a/docs/guides/add-ebean-postgres-database-config.md b/docs/guides/add-ebean-postgres-database-config.md
index 43c229b88..7e06ffb7b 100644
--- a/docs/guides/add-ebean-postgres-database-config.md
+++ b/docs/guides/add-ebean-postgres-database-config.md
@@ -4,7 +4,7 @@
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 2 of 2.
+with Ebean's `DataSourceBuilder`. Follow every step in order. This is Step 2 of 3.
---
@@ -280,3 +280,11 @@ validation best practices, see the [ebean-datasource guides](https://github.com/
| `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 |
+
+---
+
+## Next Step
+
+Proceed to **Step 3: Test container setup**
+(`add-ebean-postgres-test-container.md`) to wire an injectable test `Database`
+backed by `ebean-test` containers.
diff --git a/docs/guides/add-ebean-postgres-maven-pom.md b/docs/guides/add-ebean-postgres-maven-pom.md
index 520db55c7..a049cc5e6 100644
--- a/docs/guides/add-ebean-postgres-maven-pom.md
+++ b/docs/guides/add-ebean-postgres-maven-pom.md
@@ -3,7 +3,7 @@
## 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 2.
+to add Ebean ORM with PostgreSQL support. Follow every step in order. This is Step 1 of 3.
---
diff --git a/docs/guides/entity-bean-creation.md b/docs/guides/entity-bean-creation.md
index 713af3d01..5b7ba6d52 100644
--- a/docs/guides/entity-bean-creation.md
+++ b/docs/guides/entity-bean-creation.md
@@ -2,7 +2,7 @@
**Target Audience:** AI systems (Claude, Copilot, ChatGPT, etc.)
**Purpose:** Learn how to generate clean, idiomatic Ebean entity beans
-**Key Insight:** Ebean requires private fields with getters/setters (or other accessors), but they don't need to follow Java bean conventions; no public fields; no equals/hashCode implementation needed
+**Key Insight:** Ebean entity fields must be non-public (no public fields). Getters/setters are optional, and if used they don't need JavaBeans naming conventions; no manual equals/hashCode implementation is needed
**Language:** Java
**Framework:** Ebean ORM
@@ -16,7 +16,7 @@ Before writing entity code, remember:
|-------------|---------|-----------------------------------------------------------------------------------------------------------------------|
| `@Entity` annotation | ✅ **YES** | Marks class as persistent entity |
| `@Id` annotation | ✅ **YES** | Marks primary key field |
-| Getters/setters (or other accessors) | ✅ **YES** | Required for field access. Don't need to follow Java bean spec; can be record-style, fluent, or any accessor pattern. |
+| Getters/setters (or other accessors) | ⚠️ **OPTIONAL** | Ebean can use field access directly. Add accessors when your API/design needs them; naming can be JavaBeans, fluent, or custom. |
| 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. |
@@ -26,8 +26,8 @@ Before writing entity code, remember:
**Critical:**
- Prefer primitive `long` for `@Id` and `@Version`, NOT `Long` object.
-- Fields should be **private** or **protected** (not public). Access via getters/setters or other accessors.
-- Getters/setters do NOT need to follow Java bean conventions.
+- Fields should be non-public: **private**, **protected**, or package-private.
+- If you add accessors, they do NOT need to follow Java bean conventions.
---
@@ -112,7 +112,7 @@ public class Customer {
- ✅ `@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)
-- ✅ Getters/setters don't need to follow Java bean conventions (shown above with setName but no getId setter)
+- ✅ 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)
@@ -640,7 +640,6 @@ public class Customer {
**Why:** Ebean's naming convention handles this automatically. Only use @Column when your database column doesn't match the convention.
----
---
## What Ebean Enhancement Provides
@@ -662,7 +661,7 @@ At compile time, Ebean enhances your entity classes:
**Recommended for ID/Version:**
- `long` (primitive) ✅ Use this
- `int` (primitive) ✅ Use this
-- `uuid` (UUID) ✅ Use this
+- `UUID` ✅ Use this
**Not recommended:**
- `Long` object ⚠️ Avoid (use primitive long)
@@ -752,19 +751,19 @@ Each step adds only what's necessary. No getters/setters needed unless your desi
### Creating and saving
```java
Customer customer = new Customer();
-customer.name = "Alice";
+customer.setName("Alice");
DB.save(customer); // id auto-generated
```
### Finding
```java
Customer found = DB.find(Customer.class, 1);
-System.out.println(found.name); // Direct field access works
+System.out.println(found.getName());
```
### Updating
```java
-found.name = "Bob";
+found.setName("Bob");
DB.update(found); // version auto-incremented
```
diff --git a/docs/guides/lombok-with-ebean-entity-beans.md b/docs/guides/lombok-with-ebean-entity-beans.md
index dfb50c4c3..a0c7c599a 100644
--- a/docs/guides/lombok-with-ebean-entity-beans.md
+++ b/docs/guides/lombok-with-ebean-entity-beans.md
@@ -39,7 +39,7 @@ Problems caused:
`@Data` generates a `toString()` that accesses **all** fields, including
`@OneToMany` and `@ManyToOne` associations. Accessing an unloaded lazy association
-outside of a transaction triggers a `LazyInitializationException` or fires an unexpected
+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
diff --git a/docs/guides/persisting-and-transactions-with-ebean.md b/docs/guides/persisting-and-transactions-with-ebean.md
index e9717fd3e..70239ef2b 100644
--- a/docs/guides/persisting-and-transactions-with-ebean.md
+++ b/docs/guides/persisting-and-transactions-with-ebean.md
@@ -91,7 +91,7 @@ Customer customer = new QCustomer()
customer.setStatus(Customer.Status.ACTIVE);
-DB.save(customer);
+DB.update(customer);
```
### When to prefer `insert()` over `save()`
diff --git a/docs/guides/testing-with-testentitybuilder.md b/docs/guides/testing-with-testentitybuilder.md
index c7b9e60d5..b10ca6d8f 100644
--- a/docs/guides/testing-with-testentitybuilder.md
+++ b/docs/guides/testing-with-testentitybuilder.md
@@ -24,17 +24,19 @@ The `TestEntityBuilder` class is provided by the `ebean-test` module.
io.ebean
ebean-test
- 16.5.0
+ ${ebean.version}
test
```
**Gradle:**
```gradle
-testImplementation 'io.ebean:ebean-test:16.5.0'
+testImplementation "io.ebean:ebean-test:${ebeanVersion}"
```
-Replace `16.5.0` with the version matching your Ebean installation.
+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.
### Import the Class
@@ -64,15 +66,14 @@ The `build()` method creates an instance with populated fields **without persist
Product product = builder.build(Product.class);
// Fields are populated:
-// - id: null (not 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:
-assert product.getId() == null;
+// Not persisted yet (`@Id` is still unset until the entity is persisted).
```
### Build and Save (Persist to Database)
@@ -83,7 +84,7 @@ The `save()` method creates, persists, and returns an entity with the database-a
Product product = builder.save(Product.class);
// Entity is now in the database:
-assert product.getId() != null;
+assert DB.find(Product.class, product.getId()) != null;
// Verify it was saved:
Product found = DB.find(Product.class, product.getId());
@@ -167,7 +168,8 @@ Order order = builder.build(Order.class);
// Both order and customer are built:
assert order != null;
assert order.getCustomer() != null;
-assert order.getCustomer().getId() == null; // not persisted yet
+// 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);
@@ -253,7 +255,7 @@ 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")) {
@@ -267,14 +269,14 @@ class CompanyTestDataGenerator extends RandomValueGenerator {
}
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
+ return ThreadLocalRandom.current().nextDouble() < 0.8
+ ? OrderStatus.PENDING
: OrderStatus.COMPLETED;
}
return super.randomEnum(type);
@@ -299,7 +301,7 @@ assert user.getEmail().endsWith("@mycompany.com");
```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
@@ -346,7 +348,7 @@ When test requirements demand specific field values, manually override after bui
void whenStockIsLow_thenShowWarning() {
Product product = builder.build(Product.class);
product.setQuantity(2); // Specific value for this test
-
+
boolean shouldWarn = product.shouldShowLowStockWarning();
assertThat(shouldWarn).isTrue();
}
@@ -358,23 +360,23 @@ Encapsulate common test entity setups in factory methods:
```java
public class TestDataFactory {
-
- private static final TestEntityBuilder builder =
+
+ private static final TestEntityBuilder builder =
TestEntityBuilder.builder(DB.getDefault()).build();
-
+
public static Order createPendingOrder() {
Order order = builder.build(Order.class);
order.setStatus(OrderStatus.PENDING);
return order;
}
-
+
public static Order createShippedOrder() {
Order order = builder.build(Order.class);
order.setStatus(OrderStatus.SHIPPED);
order.setShippedAt(Instant.now());
return order;
}
-
+
public static Order savePendingOrder() {
Order order = createPendingOrder();
DB.save(order);
@@ -400,7 +402,7 @@ 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());
@@ -416,31 +418,31 @@ void whenFetchingMultipleOrders_thenAllUnique() {
```java
@SpringBootTest
class OrderRepositoryTest {
-
+
@Autowired
private OrderRepository orderRepository;
-
+
private TestEntityBuilder builder;
-
+
@BeforeEach
void setup() {
builder = TestEntityBuilder.builder(DB.getDefault()).build();
}
-
+
@Test
void whenFindingOrdersByStatus_thenReturnsMatching() {
// Create test data quickly
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);
-
+
DB.saveAll(pending1, pending2, shipped);
-
+
// Test the query
List pending = orderRepository.findByStatus(OrderStatus.PENDING);
assertThat(pending).hasSize(2);
@@ -454,12 +456,13 @@ class OrderRepositoryTest {
@Test
void whenBuildingOrderWithCustomer_thenBothPopulated() {
Order order = builder.build(Order.class);
-
+
// Customer is recursively built because of @ManyToOne(cascade=PERSIST)
assertThat(order.getCustomer()).isNotNull();
- assertThat(order.getCustomer().getId()).isNull(); // not persisted yet
+ // 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();
@@ -486,7 +489,7 @@ void usingCustomGenerator() {
TestEntityBuilder builder = TestEntityBuilder.builder(DB.getDefault())
.valueGenerator(new ECommerceTestDataGenerator())
.build();
-
+
Product product = builder.build(Product.class);
assertThat(product.getPrice())
.isBetween(BigDecimal.TEN, BigDecimal.valueOf(500.0));
@@ -510,19 +513,22 @@ public class Product {
}
```
-### Fields are null even though I expected them to be populated
+### Fields are unset even though I expected them to be populated
**Cause:** `TestEntityBuilder` does **not** populate:
-- `@Id` fields (identity/primary key)
-- `@Version` fields (optimistic locking)
+- `@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 these fields manually in your test if needed:
+**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.setId(1L); // Set @Id manually
+product.setName("specific-name"); // test-specific override
+DB.save(product); // database assigns @Id/@Version
```
### Building recursive relationships causes StackOverflowError
@@ -543,7 +549,7 @@ person.getOrganization().setFounder(null); // Break cycle
```java
class DeterministicTestDataGenerator extends RandomValueGenerator {
private int counter = 0;
-
+
@Override
protected String randomString(String propName, int maxLength) {
return "test_" + (counter++);