Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/guides/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ existing Maven project. Complete the steps in order.
| Guide | Description |
|-------|-------------|
| [Write Ebean queries with query beans](writing-ebean-query-beans.md) | Step-by-step guidance for AI agents to write type-safe Ebean queries; choose the right terminal method; tune `select()` / `fetch()` / `fetchQuery()`; and project to DTOs when entity beans are not the right output |
| [Immutable bean cache for read-only references](immutable-bean-cache.md) | Use `ImmutableBeanCache` and `ImmutableBeanCaches.loading(...)` to resolve assoc-one references in read-only/unmodifiable queries, including secondary `fetchQuery`/`fetchLazy` loads |

## Persisting & transactions

Expand Down Expand Up @@ -106,6 +107,7 @@ Key guides (fetch and follow these when performing the relevant task):
- Maven POM setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-maven-pom.md
- Database configuration: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-database-config.md
- Write queries with query beans: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/writing-ebean-query-beans.md
- Immutable bean cache for read-only references: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/immutable-bean-cache.md
- Persisting and transactions: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/persisting-and-transactions-with-ebean.md
- Test container setup: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-postgres-test-container.md
- DB migration generation: https://raw.githubusercontent.com/ebean-orm/ebean/HEAD/docs/guides/add-ebean-db-migration-generation.md
Expand Down
136 changes: 136 additions & 0 deletions docs/guides/immutable-bean-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Immutable bean cache for read-only references

This guide shows how to use `ImmutableBeanCache` for read-mostly assoc-one
references (for example `Label` references reused across many entities).

Use this when you want:

- fewer lazy-load SQL calls for assoc-one references via caching
- reusable fetch-group-based loading for cache misses

---

## Step 1 - Build an immutable cache (typical via builder)

```java
FetchGroup<Label> fetchGroup = FetchGroup.of(Label.class)
.select("version")
.fetch("labelTexts", "locale, localeText")
.build();

ImmutableBeanCache<Label> labelCache = ImmutableBeanCaches.builder(Label.class)
.loading(database, fetchGroup)
.maxSize(10_000)
.maxIdleSeconds(300)
.maxSecondsToLive(6_000)
.build();
```

`loading(...)` uses the query shape:

- `select(fetchGroup)`
- `setUnmodifiable(true)`
- `where().idIn(ids)`
- `findMap()`

Alternative domain example:

```java
FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
.select("name,version")
.fetch("billingAddress", "line1,city")
.fetch("shippingAddress", "line1,city")
.build();

ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
.loading(database, customerGroup)
.build();
```

---

## Step 2 - Attach cache to the root query

```java
AttributeDescriptor one = DB.find(AttributeDescriptor.class)
.setId(id)
.setUnmodifiable(true)
.using(labelCache)
.findOne();
```

`using(...)` is on the root query. Ebean will use this cache for matching
bean types when resolving references.

---

## Use loading helper for simple memoization

If you don't need policy controls, use the shorthand helper:

```java
ImmutableBeanCache<Label> labelCache =
ImmutableBeanCaches.loading(Label.class, database, FetchGroup.of(Label.class, "version"));
```

With `ebean-core` on the classpath, builder policy settings are backed by core
cache implementation (including periodic trim / eviction).

---

## Unmodifiable vs mutable query behavior

### Unmodifiable query path

`setUnmodifiable(true)` disables lazy loading. If you need association content
in cached beans make sure that is included in the fetch group.

```java
FetchGroup<Label> withTexts = FetchGroup.of(Label.class)
.select("version")
.fetch("labelTexts", "locale, localeText")
.build();
```

### Mutable query path

On a mutable query (no `setUnmodifiable(true)`), references populated from the
immutable cache are still mutable beans in that object graph. Additional
unloaded properties can still lazy load as normal.

Typical pattern:

1. cache serves already-loaded reference properties (for example `version`)
2. later access to unloaded properties (for example `labelTexts`) triggers
normal lazy loading

---

## Understand secondary query behavior (`+query`, `+lazy`)

When root queries execute secondary loads (`fetchQuery(...)` or `fetchLazy(...)`),
the immutable caches configured on the root query are propagated to those
secondary queries.

That means assoc-one references resolved in secondary query paths can still hit
the immutable cache.

---

## Operational note (TTL / max size)

Use `ImmutableBeanCaches.builder(...)` when you need explicit TTL/max-size
policy. `ImmutableBeanCaches.loading(...)` remains the simple helper for
loader-based memoization.


---

## Testing checklist

1. Hit / partial hit / miss behavior for `getAll(ids)`
2. Unmodifiable path: no lazy SQL when reading loaded reference properties
3. Mutable path: additional unloaded properties can still lazy load
4. Secondary `fetchQuery` and `fetchLazy` paths inherit immutable caches
5. If needed associations are in fetch group, assert no extra SQL for those
accesses
4 changes: 4 additions & 0 deletions docs/guides/writing-ebean-query-beans.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ 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.

If you need cached assoc-one references for unmodifiable graphs, see
[Immutable bean cache for read-only references](immutable-bean-cache.md).

---

## Step 7 - Use `fetchQuery()` for to-many paths and `FetchGroup` for reusable query shapes
Expand Down Expand Up @@ -481,4 +484,5 @@ When asked to add or modify an Ebean query:

- [Add Ebean Postgres Maven POM](add-ebean-postgres-maven-pom.md)
- [Entity Bean Creation](entity-bean-creation.md)
- [Immutable bean cache for read-only references](immutable-bean-cache.md)
- [Ebean query docs](https://ebean.io/docs/query/)
47 changes: 47 additions & 0 deletions ebean-api/src/main/java/io/ebean/ImmutableBeanCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.ebean;

import org.jspecify.annotations.NullMarked;
import java.util.Map;
import java.util.Set;

/**
* Query-scoped immutable bean cache.
*
* <p>Typical use is to attach an immutable cache to a query and let Ebean use it when
* resolving assoc-one references.
*
* <pre>{@code
* FetchGroup<Customer> customerGroup = FetchGroup.of(Customer.class)
* .select("name,version")
* .fetch("billingAddress", "line1,city")
* .fetch("shippingAddress", "line1,city")
* .build();
*
* ImmutableBeanCache<Customer> customerCache = ImmutableBeanCaches.builder(Customer.class)
* .loading(database, customerGroup)
* .build();
*
* Order order = database.find(Order.class)
* .setId(id)
* .setUnmodifiable(true)
* .using(customerCache)
* .findOne();
* }</pre>
*
* @param <T> The bean type.
*
* @see ImmutableBeanCaches#builder(Class)
*/
@NullMarked
public interface ImmutableBeanCache<T> {

/**
* Return the bean type this cache provides values for.
*/
Class<T> type();

/**
* Return immutable cached beans by id (loading and populating misses as needed).
*/
Map<Object, T> getAll(Set<Object> ids);
}
Loading
Loading