Skip to content

Commit 80ce7a4

Browse files
committed
Docs: guides - improve writing query beans guide
1 parent 6458bb0 commit 80ce7a4

1 file changed

Lines changed: 70 additions & 9 deletions

File tree

docs/guides/writing-ebean-query-beans.md

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ The default recommendation is:
1616

1717
1. Prefer query beans first
1818
2. Prefer entity queries for domain logic
19-
3. Prefer DTO projection for summary/read-model use cases
20-
4. Only drop to raw SQL when the ORM query cannot express the requirement cleanly
19+
3. For read-only entity graphs, prefer `setUnmodifiable(true)`
20+
4. Prefer DTO projection for summary/read-model use cases
21+
5. Only drop to raw SQL when the ORM query cannot express the requirement cleanly
2122

2223
---
2324

@@ -232,7 +233,59 @@ In this example:
232233

233234
---
234235

235-
## Step 6 - Use `fetchQuery()` for to-many paths and `FetchGroup` for reusable query shapes
236+
## Step 6 - Use `setUnmodifiable(true)` for read-only entity graphs
237+
238+
`setUnmodifiable(true)` turns the returned object graph into an unmodifiable,
239+
read-only graph.
240+
241+
This means:
242+
243+
- setters cannot mutate returned beans
244+
- associated collections are unmodifiable
245+
- lazy loading is disabled
246+
- accessing an unloaded property throws `LazyInitialisationException`
247+
- the query uses `PersistenceContextScope.QUERY`
248+
249+
### Example - read-only entity graph
250+
251+
```java
252+
private static final QCustomer CUST = QCustomer.alias();
253+
private static final QContact CONT = QContact.alias();
254+
255+
List<Customer> customers = new QCustomer()
256+
.select(CUST.name, CUST.status, CUST.whenCreated)
257+
.contacts.fetch(CONT.email)
258+
.status.equalTo(Customer.Status.ACTIVE)
259+
.setUnmodifiable(true)
260+
.findList();
261+
```
262+
263+
### When to prefer `setUnmodifiable(true)`
264+
265+
Use it when the result is meant to be read-only, such as:
266+
267+
- service/query methods returning entity graphs for display or serialization
268+
- query results you want the application to treat as immutable
269+
- cached query results or other shared read models backed by entity graphs
270+
- partial entity graphs where you want accidental lazy loading to fail fast
271+
272+
### When **not** to use it
273+
274+
Do **not** use `setUnmodifiable(true)` when the caller will:
275+
276+
- modify the beans and save them later
277+
- rely on lazy loading of associations or unloaded scalar properties
278+
- treat the result as a working persistence model rather than a read-only view
279+
280+
### Agent rule
281+
282+
If you are returning entity beans for read-only use, `setUnmodifiable(true)`
283+
should be the default recommendation. If the caller needs a mutable model or a
284+
serialized summary shape, choose mutable entities or DTO projection instead.
285+
286+
---
287+
288+
## Step 7 - Use `fetchQuery()` for to-many paths and `FetchGroup` for reusable query shapes
236289

237290
Ebean applies important SQL rules when translating ORM queries:
238291

@@ -292,7 +345,7 @@ plain `fetch(...)` on those paths. `fetchQuery()` is often the safer default.
292345

293346
---
294347

295-
## Step 7 - Use DTO projection when the caller does not need entity beans
348+
## Step 8 - Use DTO projection when the caller does not need entity beans
296349

297350
For list screens, API summaries, exports, or read-model views, the caller often
298351
does **not** need managed entity beans. In those cases, project directly to a
@@ -323,7 +376,7 @@ List<CustomerSummary> summaries = new QCustomer()
323376

324377
---
325378

326-
## Step 8 - Only fall back to raw SQL when the ORM query is not a good fit
379+
## Step 9 - Only fall back to raw SQL when the ORM query is not a good fit
327380

328381
Prefer the following order:
329382

@@ -382,7 +435,12 @@ Customer customer = new QCustomer()
382435
If the caller only needs summary fields, return a DTO instead of partially
383436
loaded entities that might later trigger more loading or confuse serializers.
384437

385-
### Anti-pattern 4 - Fetching every relationship "just in case"
438+
### Anti-pattern 4 - Returning mutable entity graphs for read-only use
439+
440+
If the caller is only meant to read the result, prefer `setUnmodifiable(true)`
441+
so accidental setter calls, collection mutation, and lazy loading fail fast.
442+
443+
### Anti-pattern 5 - Fetching every relationship "just in case"
386444

387445
Do not eagerly fetch large object graphs unless the immediate caller will use
388446
them. Query tuning is part of the job.
@@ -398,6 +456,8 @@ them. Query tuning is part of the job.
398456
| Old `Q*` class still appears after entity rename | Stale generated source/class output | Run a clean rebuild |
399457
| `findOne()` fails because multiple rows match | Predicate is not unique | Use `findList()` or tighten the predicate |
400458
| Returned entities only have some fields loaded | `select()` or `FetchGroup` limited the query shape | Add the required fields or switch to DTO projection |
459+
| 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 |
460+
| Accessing an unloaded property throws `LazyInitialisationException` | `setUnmodifiable(true)` disables lazy loading | Fetch the property up front or use DTO projection |
401461
| 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 |
402462

403463
---
@@ -410,9 +470,10 @@ When asked to add or modify an Ebean query:
410470
2. Choose the terminal method first (`exists`, `findOne`, `findList`, `findPagedList`, `asDto`)
411471
3. Add predicates with query bean properties and association traversal
412472
4. Add explicit ordering and pagination if relevant
413-
5. Tune the fetch shape with `select()` / `fetch()` / `fetchQuery()` / `FetchGroup`
414-
6. Prefer DTO projection for read models and serialized responses
415-
7. Only use raw SQL if the ORM query is genuinely the wrong tool
473+
5. If the result is read-only entity data, consider `setUnmodifiable(true)`
474+
6. Tune the fetch shape with `select()` / `fetch()` / `fetchQuery()` / `FetchGroup`
475+
7. Prefer DTO projection for read models and serialized responses
476+
8. Only use raw SQL if the ORM query is genuinely the wrong tool
416477

417478
---
418479

0 commit comments

Comments
 (0)