From 6d9ed670ef22cdd306511737d87bf5b4bcca1233 Mon Sep 17 00:00:00 2001 From: "robin.bygrave" Date: Thu, 16 Apr 2026 16:48:27 +1200 Subject: [PATCH] Docs: Improve doc guides wording --- README.md | 2 + docs/guides/README.md | 2 +- .../add-ebean-postgres-database-config.md | 10 ++- docs/guides/add-ebean-postgres-maven-pom.md | 2 +- docs/guides/entity-bean-creation.md | 19 ++--- docs/guides/lombok-with-ebean-entity-beans.md | 2 +- .../persisting-and-transactions-with-ebean.md | 2 +- docs/guides/testing-with-testentitybuilder.md | 82 ++++++++++--------- 8 files changed, 68 insertions(+), 53 deletions(-) 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++);