diff --git a/docs/guides/entity-bean-creation.md b/docs/guides/entity-bean-creation.md index a63d5917b..dcf57688c 100644 --- a/docs/guides/entity-bean-creation.md +++ b/docs/guides/entity-bean-creation.md @@ -376,7 +376,12 @@ public class Customer { **Important:** - Use `List<>` not `Set<>` for collections (Set calls equals/hashCode before beans have IDs) - `mappedBy` means Order.customer is the owner -- Relationships are lazy-loaded by default +- Relationships are lazy-loaded by default — **except** `@OneToOne(mappedBy=...)`, + which defaults to `FetchType.EAGER` and adds a `left join` to every default + select of the owning entity. Explicitly set `fetch = FetchType.LAZY` on + `@OneToOne(mappedBy=...)` fields unless the association is needed on + (almost) every load. See "`@OneToOne(mappedBy=...)` is EAGER by default" in + the query-beans guide for details. --- diff --git a/docs/guides/writing-ebean-query-beans.md b/docs/guides/writing-ebean-query-beans.md index 62bc6297c..6fbc93d19 100644 --- a/docs/guides/writing-ebean-query-beans.md +++ b/docs/guides/writing-ebean-query-beans.md @@ -431,6 +431,48 @@ List customers = new QCustomer() If the caller needs multiple to-many paths or a paged query, be suspicious of a plain `fetch(...)` on those paths. `fetchQuery()` is often the safer default. +### `@OneToOne(mappedBy=...)` is EAGER by default — mark it LAZY + +The non-owning side of a `@OneToOne` (the side with `mappedBy`) defaults to +`FetchType.EAGER` per JPA, same as `@ManyToOne`. Unlike a `@ManyToOne` +reference (which is FK-only until `.fetch()`'d), Ebean's default select for an +EAGER `@OneToOne(mappedBy=...)` still adds a `left join` to the target table +on **every** query for the owning entity — even a plain `findById()` — because +there is no local FK column to use as a lazy reference; the only way to know +the associated row exists is to join to it. + +If that association is rarely needed (e.g. a rarely-read child/detail table), +this join executes on every load of the parent, including in hot-path list +queries, and can dominate query cost as more such associations accumulate. + +**Always set `fetch = FetchType.LAZY` on `@OneToOne(mappedBy=...)` +associations unless the association is genuinely needed on (almost) every +load:** + +```java +@OneToOne(mappedBy = "device", fetch = FetchType.LAZY) +private SensorBoard sensorBoard; +``` + +This correctly excludes the join from Ebean's default select clause (verified +for FK-based, non-shared-primary-key `@OneToOne` relationships — the common +case). Callers that do need the association can still `.fetch("sensorBoard")` +explicitly on the query bean. + +**Caveat:** the exclusion is driven by Ebean's default-select-clause +mechanism. It is bypassed if the query has already been switched into an +"all properties" mode by something other than the deploy-time +`FetchType.LAZY`/`EAGER` metadata (for example, an active AutoTune profile +that supplies its own tuned property set). Confirm the join is actually gone +by checking generated SQL (`LoggedSql` in tests, or query logging) after +making this change — don't assume it's excluded from the annotation alone. + +### Agent rule + +Default new `@OneToOne(mappedBy=...)` fields to `fetch = FetchType.LAZY` +unless there's a clear reason the association is needed on every load. This +is a one-line, low-risk change that avoids an always-on join. + --- ## Step 8 - Use DTO projection when the caller does not need entity beans