mirror of
https://github.com/ebean-orm/ebean.git
synced 2026-09-20 11:17:36 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72ce587629 | ||
|
|
77e98a3f54 | ||
|
|
3c4e87f3f7 | ||
|
|
f5eee70ec4 |
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -431,6 +431,48 @@ List<Customer> 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
|
||||
|
||||
@@ -9,6 +9,11 @@ import java.time.Instant;
|
||||
*/
|
||||
public interface MetaQueryPlan {
|
||||
|
||||
/**
|
||||
* Return the name of the database for the query.
|
||||
*/
|
||||
String dbName();
|
||||
|
||||
/**
|
||||
* Return the bean type for the query.
|
||||
*/
|
||||
|
||||
@@ -80,6 +80,10 @@ public final class DtoMappingRequest {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String dbName() {
|
||||
return server.name();
|
||||
}
|
||||
|
||||
public String hash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
|
||||
private final QueryPlanMetric planMetric;
|
||||
private final TimedMetric metric;
|
||||
private final Class<?> beanType;
|
||||
private final String dbName;
|
||||
private final String name;
|
||||
private final String hash;
|
||||
private final String sql;
|
||||
@@ -26,6 +27,7 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
|
||||
this.planMetric = request.createMetric();
|
||||
this.metric = planMetric.metric();
|
||||
this.beanType = request.type();
|
||||
this.dbName = request.dbName();
|
||||
this.name = request.name();
|
||||
this.hash = request.hash();
|
||||
this.sql = request.sql();
|
||||
@@ -91,6 +93,6 @@ abstract class DtoQueryPlanBase implements DtoQueryPlan, SpiQueryPlan {
|
||||
|
||||
@Override
|
||||
public SpiDbQueryPlan createMeta(String bind, String planString) {
|
||||
return new DQueryPlanOutput(beanType, name, hash, sql, profileLocation, bind, planString);
|
||||
return new DQueryPlanOutput(beanType, dbName, name, hash, sql, profileLocation, bind, planString);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,13 @@ import io.ebean.meta.MetricVisitor;
|
||||
import io.ebean.metric.CountMetric;
|
||||
import io.ebean.metric.CountMetricStats;
|
||||
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
/**
|
||||
* Used to collect counter metrics.
|
||||
*/
|
||||
final class DCountMetric implements CountMetric {
|
||||
|
||||
private final String name;
|
||||
private final LongAdder count = new LongAdder();
|
||||
private final ValueAdder count = new ValueAdder();
|
||||
private String reportName;
|
||||
|
||||
DCountMetric(String name) {
|
||||
@@ -29,12 +27,12 @@ final class DCountMetric implements CountMetric {
|
||||
|
||||
@Override
|
||||
public void increment() {
|
||||
count.increment();
|
||||
count.add(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return count.sum() == 0;
|
||||
return count.currentValue() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,12 +42,12 @@ final class DCountMetric implements CountMetric {
|
||||
|
||||
@Override
|
||||
public long get(boolean reset) {
|
||||
return reset ? count.sumThenReset() : count.sum();
|
||||
return count.get(reset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(MetricVisitor visitor) {
|
||||
long val = visitor.reset() ? count.sumThenReset() : count.sum();
|
||||
long val = count.get(visitor.reset());
|
||||
if (val > 0) {
|
||||
final String name = reportName != null ? reportName : reportName(visitor);
|
||||
visitor.visitCount(new DCountMetricStats(name, val));
|
||||
|
||||
@@ -4,7 +4,6 @@ import io.ebean.meta.MetricVisitor;
|
||||
import io.ebean.metric.TimedMetric;
|
||||
|
||||
import java.util.concurrent.atomic.LongAccumulator;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
/**
|
||||
* Used to collect timed execution statistics.
|
||||
@@ -15,8 +14,8 @@ import java.util.concurrent.atomic.LongAdder;
|
||||
final class DTimedMetric implements TimedMetric {
|
||||
|
||||
private final String name;
|
||||
private final LongAdder count = new LongAdder();
|
||||
private final LongAdder total = new LongAdder();
|
||||
private final ValueAdder count = new ValueAdder();
|
||||
private final ValueAdder total = new ValueAdder();
|
||||
private final LongAccumulator max = new LongAccumulator(Math::max, 0);
|
||||
private boolean collected;
|
||||
private String reportName;
|
||||
@@ -43,14 +42,14 @@ final class DTimedMetric implements TimedMetric {
|
||||
|
||||
@Override
|
||||
public void add(long value) {
|
||||
count.increment();
|
||||
count.add(1);
|
||||
total.add(value);
|
||||
max.accumulate(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return count.sum() == 0;
|
||||
return count.currentValue() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,16 +61,17 @@ final class DTimedMetric implements TimedMetric {
|
||||
|
||||
@Override
|
||||
public void visit(MetricVisitor visitor) {
|
||||
final long countSum = visitor.reset() ? count.sumThenReset() : count.sum();
|
||||
final boolean reset = visitor.reset();
|
||||
final long countSum = count.get(reset);
|
||||
if (countSum > 0) {
|
||||
final String name = reportName != null ? reportName : reportName(visitor);
|
||||
visitor.visitTimed(stats(visitor.reset(), name, countSum));
|
||||
visitor.visitTimed(stats(reset, name, countSum));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DTimeMetricStats collect(boolean reset) {
|
||||
final long countSum = reset ? count.sumThenReset() : count.sum();
|
||||
final long countSum = count.get(reset);
|
||||
if (countSum == 0) {
|
||||
return null;
|
||||
} else {
|
||||
@@ -84,7 +84,7 @@ final class DTimedMetric implements TimedMetric {
|
||||
*/
|
||||
private DTimeMetricStats stats(boolean reset, String name, long countSum) {
|
||||
try {
|
||||
final long totalSum = reset ? total.sumThenReset() : total.sum();
|
||||
final long totalSum = total.get(reset);
|
||||
return new DTimeMetricStats(name, collected, countSum, totalSum, max.getThenReset());
|
||||
} finally {
|
||||
collected = true;
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.ebeaninternal.server.profile;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
/**
|
||||
* Accumulates a value while supporting cumulative and reset-based delta reads.
|
||||
*/
|
||||
final class ValueAdder {
|
||||
|
||||
private final LongAdder value = new LongAdder();
|
||||
private final AtomicLong previousValue = new AtomicLong();
|
||||
|
||||
void add(long amount) {
|
||||
value.add(amount);
|
||||
}
|
||||
|
||||
long get(boolean reset) {
|
||||
long currentValue = value.sum();
|
||||
if (!reset) {
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
long previous = previousValue.getAndSet(currentValue);
|
||||
return currentValue >= previous ? currentValue - previous : currentValue;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
value.reset();
|
||||
previousValue.set(0);
|
||||
}
|
||||
|
||||
long currentValue() {
|
||||
return value.sum();
|
||||
}
|
||||
}
|
||||
@@ -274,7 +274,7 @@ public class CQueryPlan implements SpiQueryPlan {
|
||||
|
||||
@Override
|
||||
public final DQueryPlanOutput createMeta(String bind, String planString) {
|
||||
return new DQueryPlanOutput(beanType(), name, hash, sql, profileLocation, bind, planString);
|
||||
return new DQueryPlanOutput(beanType(), server.name(), name, hash, sql, profileLocation, bind, planString);
|
||||
}
|
||||
|
||||
public DataReader createDataReader(boolean unmodifiable, ResultSet rset) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.time.Instant;
|
||||
public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
|
||||
|
||||
private final Class<?> beanType;
|
||||
private final String dbName;
|
||||
private final String label;
|
||||
private final ProfileLocation profileLocation;
|
||||
|
||||
@@ -25,8 +26,9 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
|
||||
private Instant whenCaptured;
|
||||
private Object tenantId;
|
||||
|
||||
public DQueryPlanOutput(Class<?> beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
|
||||
public DQueryPlanOutput(Class<?> beanType, String dbName, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) {
|
||||
this.beanType = beanType;
|
||||
this.dbName = dbName;
|
||||
this.label = label;
|
||||
this.hash = hash;
|
||||
this.sql = sql;
|
||||
@@ -35,6 +37,11 @@ public final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan {
|
||||
this.plan = plan;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String dbName() {
|
||||
return dbName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String hash() {
|
||||
return hash;
|
||||
|
||||
@@ -21,11 +21,13 @@ public final class SqlQueryPlan implements SpiQueryPlan {
|
||||
private final String name;
|
||||
private final String hash;
|
||||
private final String sql;
|
||||
private final String dbName;
|
||||
private final SpiQueryBindCapture bindCapture;
|
||||
|
||||
SqlQueryPlan(SpiEbeanServer server, String name, String sql) {
|
||||
this.name = name;
|
||||
this.sql = sql;
|
||||
this.dbName = server.name();
|
||||
this.hash = Md5.hash(sql, name);
|
||||
this.bindCapture = server.createQueryBindCapture(this);
|
||||
}
|
||||
@@ -77,6 +79,6 @@ public final class SqlQueryPlan implements SpiQueryPlan {
|
||||
|
||||
@Override
|
||||
public SpiDbQueryPlan createMeta(String bind, String planString) {
|
||||
return new DQueryPlanOutput(null, name, hash, sql, null, bind, planString);
|
||||
return new DQueryPlanOutput(null, dbName, name, hash, sql, null, bind, planString);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,4 +40,19 @@ class DCountMetricTest {
|
||||
assertThat(result2.get(0).count()).isEqualTo(12);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void cumulativeAndDeltaAreIndependent() {
|
||||
DCountMetric counter = new DCountMetric("org.hello");
|
||||
counter.add(7);
|
||||
|
||||
assertThat(counter.get(false)).isEqualTo(7);
|
||||
assertThat(counter.get(false)).isEqualTo(7);
|
||||
assertThat(counter.get(true)).isEqualTo(7);
|
||||
|
||||
counter.add(5);
|
||||
assertThat(counter.get(false)).isEqualTo(12);
|
||||
assertThat(counter.get(true)).isEqualTo(5);
|
||||
assertThat(counter.get(true)).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,4 +116,25 @@ public class DTimedMetricTest {
|
||||
assertThat(stats.total()).isEqualTo(1470);
|
||||
assertThat(stats.max()).isEqualTo(160);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cumulativeAndDeltaAreIndependent() {
|
||||
DTimedMetric metric = new DTimedMetric("org.timed");
|
||||
metric.add(560);
|
||||
metric.add(500);
|
||||
|
||||
DTimeMetricStats cumulative = metric.collect(false);
|
||||
assertThat(cumulative.count()).isEqualTo(2);
|
||||
assertThat(cumulative.total()).isEqualTo(1060);
|
||||
|
||||
metric.add(160);
|
||||
|
||||
DTimeMetricStats delta = metric.collect(true);
|
||||
assertThat(delta.count()).isEqualTo(3);
|
||||
assertThat(delta.total()).isEqualTo(1220);
|
||||
|
||||
cumulative = metric.collect(false);
|
||||
assertThat(cumulative.count()).isEqualTo(3);
|
||||
assertThat(cumulative.total()).isEqualTo(1220);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ class DtoQueryPlanCaptureTest extends BaseTestCase {
|
||||
.orElse(null);
|
||||
|
||||
assertThat(dtoPlan).as("captured a native DTO query plan").isNotNull();
|
||||
assertThat(dtoPlan.dbName()).isEqualTo(DB.getDefault().name());
|
||||
assertThat(dtoPlan.sql()).contains("from o_customer where id > ?");
|
||||
assertThat(dtoPlan.plan()).isNotEmpty();
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class SqlQueryPlanCaptureTest extends BaseTestCase {
|
||||
.orElse(null);
|
||||
|
||||
assertThat(sqlPlan).as("captured a SqlQuery query plan").isNotNull();
|
||||
assertThat(sqlPlan.dbName()).isEqualTo(DB.getDefault().name());
|
||||
assertThat(sqlPlan.sql()).contains("from o_customer where id > ?");
|
||||
assertThat(sqlPlan.plan()).isNotEmpty();
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@ public class TestCustomerFinder extends BaseTestCase {
|
||||
request.maxTimeMillis(10_000);
|
||||
List<MetaQueryPlan> plans0 = server().metaInfo().queryPlanCollectNow(request);
|
||||
assertThat(plans0).isNotEmpty();
|
||||
assertThat(plans0).extracting(MetaQueryPlan::dbName).containsOnly(server().name());
|
||||
|
||||
for (MetaQueryPlan plan : plans0) {
|
||||
logger.info("queryPlan label:{}, queryTimeMicros:{} captureMicros:{} whenCaptured:{} captureCount:{} loc:{} sql:{} bind:{} plan:{}",
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
<ebean-migration.version>14.4.0</ebean-migration.version>
|
||||
<ebean-test-containers.version>8.2</ebean-test-containers.version>
|
||||
<ebean-datasource.version>10.10</ebean-datasource.version>
|
||||
<ebean-agent.version>18.3.0</ebean-agent.version>
|
||||
<ebean-maven-plugin.version>18.3.0</ebean-maven-plugin.version>
|
||||
<ebean-agent.version>18.4.0</ebean-agent.version>
|
||||
<ebean-maven-plugin.version>18.4.0</ebean-maven-plugin.version>
|
||||
<surefire.useModulePath>false</surefire.useModulePath>
|
||||
</properties>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>tests</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.4.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>test-dto-mapping</artifactId>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<artifactId>tests</artifactId>
|
||||
<groupId>io.ebean</groupId>
|
||||
<version>18.3.0</version>
|
||||
<version>18.4.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>test-java16</artifactId>
|
||||
|
||||
Reference in New Issue
Block a user