#1863 - L2 cache miss on natural key cache when multiple properties that include a ManyToOne

This commit is contained in:
rob bygrave
2019-11-16 17:03:34 +13:00
parent 738f19491a
commit dfefbecf44
16 changed files with 368 additions and 49 deletions
+9
View File
@@ -2,6 +2,7 @@ package io.ebean;
import io.ebean.annotation.PersistBatch;
import io.ebean.annotation.Platform;
import io.ebean.cache.ServerCacheStatistics;
import io.ebean.config.dbplatform.IdType;
import io.ebean.meta.MetaTimedMetric;
import io.ebean.meta.MetricType;
@@ -76,6 +77,10 @@ public abstract class BaseTestCase {
}
}
protected void clearAllL2Cache() {
server().getServerCacheManager().clearAll();
}
protected void resetAllMetrics() {
server().getMetaInfoManager().resetAllMetrics();
}
@@ -221,6 +226,10 @@ public abstract class BaseTestCase {
return spiEbeanServer().getBeanDescriptor(cls);
}
protected <T> ServerCacheStatistics getBeanCacheStats(Class<T> cls, boolean reset) {
return server().getServerCacheManager().getBeanCache(cls).getStatistics(reset);
}
protected Platform platform() {
return spiEbeanServer().getDatabasePlatform().getPlatform();
}
@@ -0,0 +1,33 @@
package org.tests.model.basic.cache;
import io.ebean.Model;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
@MappedSuperclass
public class OCacheBase extends Model {
@Id
private long id;
@Version
private long version;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public long getVersion() {
return version;
}
public void setVersion(long version) {
this.version = version;
}
}
@@ -0,0 +1,22 @@
package org.tests.model.basic.cache;
import io.ebean.annotation.Cache;
import javax.persistence.Entity;
import javax.persistence.UniqueConstraint;
@Cache(naturalKey = "appName")
@Entity
@UniqueConstraint(columnNames = "app_name")
public class OCachedApp extends OCacheBase {
private final String appName;
public OCachedApp(String appName) {
this.appName = appName;
}
public String getAppName() {
return appName;
}
}
@@ -0,0 +1,31 @@
package org.tests.model.basic.cache;
import io.ebean.annotation.Cache;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import javax.persistence.UniqueConstraint;
@Cache(naturalKey = {"app", "detail"})
@Entity
@UniqueConstraint(columnNames = {"app_id", "detail"})
public class OCachedAppDetail extends OCacheBase {
@ManyToOne(optional = false)
private final OCachedApp app;
private final String detail;
public OCachedAppDetail(OCachedApp app, String detail) {
this.app = app;
this.detail = detail;
}
public OCachedApp getApp() {
return app;
}
public String getDetail() {
return detail;
}
}
@@ -0,0 +1,126 @@
package org.tests.model.basic.cache;
import io.ebean.BaseTestCase;
import io.ebean.DB;
import io.ebean.cache.ServerCacheStatistics;
import org.ebeantest.LoggedSqlCollector;
import org.junit.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class TestNatKeyCacheWithForeignKey extends BaseTestCase {
private static boolean seededData;
private static final OCachedApp app0 = new OCachedApp("app0");
private static final OCachedApp app1 = new OCachedApp("app1");
private ServerCacheStatistics getStats() {
return getBeanCacheStats(OCachedAppDetail.class, true);
}
@Test
public void test_findOne() {
setupData();
clearAllL2Cache();
final OCachedAppDetail found0 = findDetail(app0, "detail0");
assertThat(found0).isNotNull();
assertThat(getStats().getHitCount()).isEqualTo(0);
resetAllMetrics();
LoggedSqlCollector.start();
final OCachedAppDetail found1 = findDetail(app0, "detail0");
assertThat(found1).isNotNull();
final List<String> sql = LoggedSqlCollector.stop();
assertThat(sql).as("Expected cache hit, no SQL query expected").isEmpty();
assertThat(getStats().getHitCount()).isEqualTo(1);
}
private OCachedAppDetail findDetail(OCachedApp app, String detail) {
return DB.find(OCachedAppDetail.class)
.where()
.eq("app", app)
.eq("detail", detail)
.findOne();
}
@Test
public void test_findList_details_expect_hitNatKeyCache() {
setupData();
clearAllL2Cache();
final List<OCachedAppDetail> result0 = findListDetails(app0, "detail0", "detail1");
assertThat(result0).hasSize(2);
assertThat(getStats().getHitCount()).isEqualTo(0);
LoggedSqlCollector.start();
final List<OCachedAppDetail> result1 = findListDetails(app0, "detail0", "detail1");
assertThat(result1).hasSize(2);
final List<String> sql = LoggedSqlCollector.stop();
assertThat(sql).as("Expected cache hit, no SQL query expected").isEmpty();
assertThat(getStats().getHitCount()).isEqualTo(2);
}
private List<OCachedAppDetail> findListDetails(OCachedApp app, String... details) {
return DB.find(OCachedAppDetail.class)
.setUseCache(true)
.where()
.eq("app", app)
.in("detail", details)
.findList();
}
@Test
public void test_findList_foreignKey_expect_hitNatKeyCache() {
setupData();
clearAllL2Cache();
final List<OCachedAppDetail> result0 = findListApps("detail0", app0, app1);
assertThat(result0).hasSize(2);
assertThat(getStats().getHitCount()).isEqualTo(0);
LoggedSqlCollector.start();
final List<OCachedAppDetail> result1 = findListApps("detail0", app0, app1);
assertThat(result1).hasSize(2);
final List<String> sql = LoggedSqlCollector.stop();
assertThat(sql).as("Expected cache hit, no SQL query expected").isEmpty();
assertThat(getStats().getHitCount()).isEqualTo(2);
}
private List<OCachedAppDetail> findListApps(String detail, OCachedApp... apps) {
return DB.find(OCachedAppDetail.class)
.setUseCache(true)
.where()
.in("app", apps)
.eq("detail", detail)
.findList();
}
private static void setupData() {
if (!seededData) {
seededData = true;
app0.save();
app1.save();
new OCachedAppDetail(app0, "detail0").save();
new OCachedAppDetail(app0, "detail1").save();
new OCachedAppDetail(app1, "detail0").save();
new OCachedAppDetail(app1, "detail1").save();
}
}
}