Docs: Improve doc guides wording

This commit is contained in:
robin.bygrave
2026-04-16 16:48:27 +12:00
parent 9e2d821475
commit 6d9ed670ef
8 changed files with 68 additions and 53 deletions
+2
View File
@@ -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:
+1 -1
View File
@@ -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
@@ -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.
+1 -1
View File
@@ -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.
---
+9 -10
View File
@@ -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
```
@@ -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
@@ -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()`
+44 -38
View File
@@ -24,17 +24,19 @@ The `TestEntityBuilder` class is provided by the `ebean-test` module.
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-test</artifactId>
<version>16.5.0</version>
<version>${ebean.version}</version>
<scope>test</scope>
</dependency>
```
**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<Order> 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++);