From afc789db583abd8840f66d6de4ddb96637082d21 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 19 Jan 2023 14:30:56 +0100 Subject: [PATCH] NEW: Support for queryCache on find-by-id --- .../server/core/DefaultServer.java | 33 +++++-- .../server/query/DefaultOrmQueryEngine.java | 9 +- .../java/org/tests/cache/TestQueryCache.java | 88 +++++++++++++++++++ 3 files changed, 122 insertions(+), 8 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java index 5d3410e38e..4296511e87 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultServer.java @@ -1001,6 +1001,7 @@ public PersistenceContextScope persistenceContextScope(SpiQuery query) { private T findId(Query query, @Nullable Transaction transaction) { SpiQuery spiQuery = (SpiQuery) query; spiQuery.setType(Type.BEAN); + SpiOrmQueryRequest request = null; if (SpiQuery.Mode.NORMAL == spiQuery.getMode() && !spiQuery.isForceHitDatabase()) { // See if we can skip doing the fetch completely by getting the bean from the // persistence context or the bean cache @@ -1023,16 +1024,36 @@ private T findId(Query query, @Nullable Transaction transaction) { } } } - if (spiQuery.isBeanCacheGet() && (t == null || !t.isSkipCache())) { - // Hit the L2 bean cache - T bean = desc.cacheBeanGet(id, spiQuery.isReadOnly(), pc); - if (bean != null) { - return bean; + if (t == null || !t.isSkipCache()) { + if (spiQuery.getUseQueryCache() != CacheMode.OFF) { + request = buildQueryRequest(spiQuery, transaction); + if (request.isQueryCacheActive()) { + // Hit the query cache + request.prepareQuery(); + T bean = request.getFromQueryCache(); + if (bean != null) { + return bean; + } + } + } + if (spiQuery.isBeanCacheGet()) { + // Hit the L2 bean cache + T bean = desc.cacheBeanGet(id, spiQuery.isReadOnly(), pc); + if (bean != null) { + if (request != null && request.isQueryCachePut()) { + // copy bean from the L2 cache to the faster query cache, if caching is enabled + request.prepareQuery(); + request.putToQueryCache(bean); + } + return bean; + } } } } - SpiOrmQueryRequest request = buildQueryRequest(spiQuery, transaction); + if (request == null) { + request = buildQueryRequest(spiQuery, transaction); + } request.prepareQuery(); if (request.isUseDocStore()) { return docStore().find(request); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java index 98a2ca471c..9711aab7a0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultOrmQueryEngine.java @@ -163,8 +163,13 @@ public T findId(OrmQueryRequest request) { if (finder != null) { result = finder.postProcess(request, result); } - if (result != null && request.isBeanCachePut()) { - request.descriptor().cacheBeanPut((EntityBean) result); + if (result != null) { + if (request.isBeanCachePut()) { + request.descriptor().cacheBeanPut((EntityBean) result); + } + if (request.isQueryCachePut()) { + request.putToQueryCache(result); + } } return result; } diff --git a/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java b/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java index 6970dce776..ed68ef88bb 100644 --- a/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java +++ b/ebean-test/src/test/java/org/tests/cache/TestQueryCache.java @@ -3,12 +3,14 @@ import io.ebean.CacheMode; import io.ebean.DB; import io.ebean.ExpressionList; +import io.ebean.Query; import io.ebean.annotation.Transactional; import io.ebean.annotation.TxIsolation; import io.ebean.bean.BeanCollection; import io.ebean.cache.ServerCache; import io.ebean.test.LoggedSql; import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; import org.tests.model.basic.ResetBasicData; @@ -302,6 +304,92 @@ public void findCountFirstRecacheThenOn() { } + /** + * This test primarily checks, if the query cache on findById will work properly. + * + * It also checks some special cases, if query + bean cache are combined with other queries. + * It is important to know, that a "findId" query, that is not yet compiled will use the bean cache. + */ + @Test + @SuppressWarnings("unchecked") + public void testFindByIdWihtBothCaches() { + + ResetBasicData.reset(); + + List ids = DB.find(Customer.class).setMaxRows(5).findIds(); + Customer c = DB.find(Customer.class).setMaxRows(1).findOne(); + + DB.getDefault().pluginApi().cacheManager().clearAll(); + + ServerCache bc = DB.getDefault().pluginApi().cacheManager().beanCache(Customer.class); + ServerCache qc = DB.getDefault().pluginApi().cacheManager().queryCache(Customer.class); + bc.statistics(true); + qc.statistics(true); + + // 1. load the bean cache with some beans + DB.find(Customer.class).where().idIn(ids).findList(); + + Query q = DB.find(Customer.class).setUseQueryCache(true).setUseCache(true).setReadOnly(true); + LoggedSql.start(); + Customer c1 = q.copy().where().eq("name", c.getName()).findOne(); + Customer c2 = q.copy().where().eq("name", c.getName()).findOne(); + assertThat(LoggedSql.stop()).hasSize(1); + + assertTrue(DB.beanState(c1).isReadOnly()); + assertTrue(DB.beanState(c2).isReadOnly()); + assertThat(c1).isSameAs(c2); + assertThat(bc.statistics(true).getHitCount()).isEqualTo(0); + assertThat(qc.statistics(true).getHitCount()).isEqualTo(1); + + + LoggedSql.start(); + c1 = q.copy().where().eq("id", c.getId()).findOne(); + c2 = q.copy().where().eq("id", c.getId()).findOne(); + assertThat(LoggedSql.stop()).isEmpty(); + assertThat(c1).isNotSameAs(c2); + + LoggedSql.start(); + c1 = q.copy().setId(c.getId()).findOne(); + c2 = q.copy().setId(c.getId()).findOne(); + assertThat(LoggedSql.stop()).isEmpty(); + assertThat(c1).isNotSameAs(c2); + + LoggedSql.start(); + List l1 = q.copy().where().idIn(ids.subList(0,2)).findList(); + List l2 = q.copy().where().idIn(ids.subList(0,2)).findList(); + assertThat(LoggedSql.stop()).isEmpty(); + assertThat(l1).hasSize(2).isNotSameAs(l2); + + assertThat(bc.statistics(true).getHitCount()).isEqualTo(8); // 4x findOne and 2x findList with 2 elements + assertThat(qc.statistics(true).getHitCount()).isEqualTo(0); + // Note: The ID queries are immediately handled by the BeanCache, because the underlying queries are never compiled + // and so they have no query plan, which is required for cache access. + // + // So we clear the cache and try it again + DB.getDefault().pluginApi().cacheManager().clearAll(); + + LoggedSql.start(); + c1 = q.copy().where().eq("id", c.getId()).findOne(); + c2 = q.copy().where().eq("id", c.getId()).findOne(); + assertThat(LoggedSql.stop()).hasSize(1); + assertThat(c1).isSameAs(c2); + + LoggedSql.start(); + c1 = q.copy().setId(c.getId()).findOne(); + c2 = q.copy().setId(c.getId()).findOne(); + assertThat(LoggedSql.stop()).isEmpty(); // setId(..) query has same queryPlan as eq("id", ..) + assertThat(c1).isSameAs(c2); + + LoggedSql.start(); + l1 = q.copy().where().idIn(1, 2).findList(); + l2 = q.copy().where().idIn(1, 2).findList(); + assertThat(LoggedSql.stop()).hasSize(1); // we have to hit DB for bean#2 + assertThat(l1).hasSize(2).isSameAs(l2); + + assertThat(bc.statistics(true).getHitCount()).isEqualTo(1); // findList can get one bean from bc + assertThat(qc.statistics(true).getHitCount()).isEqualTo(4); // 6 queries, 2 of them hit db + } + @Test @SuppressWarnings("unchecked") public void testReadOnlyFind() {