From c31cac73795c6c302286882b01b807d8dd04d303 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 14:27:02 +0200 Subject: [PATCH 01/36] Bug: fetch and where with formula join will produce incorrect joins --- .../server/query/SqlTreeBuilder.java | 5 -- .../org/tests/model/family/ChildPerson.java | 2 +- .../query/joins/TestQueryJoinOnFormula.java | 73 +++++++++++++++++++ 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java index 54fbe19693..8ddd51f03e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeBuilder.java @@ -677,11 +677,6 @@ private SqlTreeNodeExtraJoin findExtraJoinRoot(String includeProp, SqlTreeNodeEx } else { // look in register ... String parentPropertyName = includeProp.substring(0, dotPos); - if (selectIncludes.contains(parentPropertyName)) { - // parent already handled by select - return childJoin; - } - SqlTreeNodeExtraJoin parentJoin = joinRegister.get(parentPropertyName); if (parentJoin == null) { // we need to create this the parent implicitly... diff --git a/ebean-test/src/test/java/org/tests/model/family/ChildPerson.java b/ebean-test/src/test/java/org/tests/model/family/ChildPerson.java index 2f9f718d4a..ec61ec0c22 100644 --- a/ebean-test/src/test/java/org/tests/model/family/ChildPerson.java +++ b/ebean-test/src/test/java/org/tests/model/family/ChildPerson.java @@ -29,7 +29,7 @@ public class ChildPerson extends InheritablePerson { @Formula(select = "coalesce(${ta}.address, j1.address, j2.address)", join = PARENTS_JOIN) private String effectiveAddress; - @Formula(select = "coalesce(${ta}.some_bean_id, j1.some_bean_id, j2.some_bean_id)") + @Formula(select = "coalesce(${ta}.some_bean_id, j1.some_bean_id, j2.some_bean_id)", join = PARENTS_JOIN) @ManyToOne private EBasic effectiveBean; diff --git a/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java b/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java index 15b50226e9..0c985ec40e 100644 --- a/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java +++ b/ebean-test/src/test/java/org/tests/query/joins/TestQueryJoinOnFormula.java @@ -333,6 +333,79 @@ public void test_ChildPersonParentFindCount() { assertThat(loggedSql.get(0)).contains("where coalesce(f2.child_age, 0) = ?"); } + @Test + public void test_fetch_only() { + + LoggedSql.start(); + + DB.find(ChildPerson.class).select("name").fetch("parent.effectiveBean").findList(); + + List loggedSql = LoggedSql.stop(); + assertThat(loggedSql.get(0)).contains("from child_person t0 " + + "left join parent_person t1 on t1.identifier = t0.parent_identifier " + + "left join grand_parent_person j1 on j1.identifier = t1.parent_identifier " + + "left join e_basic t2 on t2.id = coalesce(t1.some_bean_id, j1.some_bean_id)"); + } + + @Test + public void test_where_only() { + + LoggedSql.start(); + + DB.find(ChildPerson.class).select("name") + .where().eq("parent.effectiveBean.name", "foo") + .findList(); + + List loggedSql = LoggedSql.stop(); + assertThat(loggedSql.get(0)) + .contains("from child_person t0 " + + "left join parent_person t1 on t1.identifier = t0.parent_identifier " + + "left join grand_parent_person j1 on j1.identifier = t1.parent_identifier " + + "left join e_basic t2 on t2.id = coalesce(t1.some_bean_id, j1.some_bean_id) " + + "where t2.name = ?"); + } + + @Test + public void test_fetch_one_prop_with_where() { + + LoggedSql.start(); + + DB.find(ChildPerson.class).select("name") + .fetch("parent", "name") + //.fetch("parent.effectiveBean", "name") + .where().eq("parent.effectiveBean.name", "foo") + .findList(); + + List loggedSql = LoggedSql.stop(); + assertThat(loggedSql.get(0)) + .contains("from child_person t0 " + + "left join parent_person t1 on t1.identifier = t0.parent_identifier " + + "left join grand_parent_person j1 on j1.identifier = t1.parent_identifier " + + "left join e_basic t2 on t2.id = coalesce(t1.some_bean_id, j1.some_bean_id)" + + " where t2.name = ?" + ); + } + + @Test + public void test_fetch_complete_with_where() { + + LoggedSql.start(); + + DB.find(ChildPerson.class).select("name") + .fetch("parent") + .where().eq("parent.effectiveBean.name", "foo") + .findList(); + + List loggedSql = LoggedSql.stop(); + assertThat(loggedSql.get(0)) + .contains("from child_person t0 " + + "left join parent_person t1 on t1.identifier = t0.parent_identifier " + + "left join (select i2.parent_identifier, count(*) as child_count, sum(i2.age) as child_age from child_person i2 group by i2.parent_identifier) f2 on f2.parent_identifier = t1.identifier " + + "left join grand_parent_person j1 on j1.identifier = t1.parent_identifier " + + "left join e_basic t2 on t2.id = coalesce(t1.some_bean_id, j1.some_bean_id) " + + "where t2.name = ?"); + } + @Test public void test_softRef() { From 7ff9bf3b38ee181d8c86f997c88fb541c1e62028 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 14:30:06 +0200 Subject: [PATCH 02/36] Optimized cache handling (QueryCache for findId, use QueryCache before BeanCache) --- .../server/core/DefaultServer.java | 119 ++++++++++-------- .../server/core/OrmQueryRequest.java | 78 ++++++++---- .../server/core/SpiOrmQueryRequest.java | 15 +++ .../io/ebeaninternal/server/query/CQuery.java | 3 - .../server/query/CQueryEngine.java | 5 - .../query/CQueryFetchSingleAttribute.java | 4 - .../server/query/CQueryRowCount.java | 4 - .../server/query/DefaultOrmQueryEngine.java | 9 +- .../tests/basic/TestDeleteByIdCollection.java | 4 + .../java/org/tests/cache/TestBeanCache.java | 113 ++++++++++++++++- .../java/org/tests/cache/TestQueryCache.java | 88 +++++++++++++ .../org/tests/model/basic/OCachedBean.java | 2 +- 12 files changed, 349 insertions(+), 95 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 3303027877..26af43658d 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 @@ -969,40 +969,6 @@ private SpiOrmQueryRequest buildQueryRequest(SpiQuery query) { return new OrmQueryRequest<>(this, queryEngine, query, transaction); } - /** - * Try to get the object out of the persistence context. - */ - @Nullable - @SuppressWarnings("unchecked") - private T findIdCheckPersistenceContextAndCache(SpiQuery query, Object id) { - SpiTransaction t = query.transaction(); - if (t == null) { - t = currentServerTransaction(); - } - BeanDescriptor desc = query.descriptor(); - id = desc.convertId(id); - PersistenceContext pc = null; - if (t != null && useTransactionPersistenceContext(query)) { - // first look in the transaction scoped persistence context - pc = t.persistenceContext(); - if (pc != null) { - WithOption o = desc.contextGetWithOption(pc, id); - if (o != null) { - if (o.isDeleted()) { - // Bean was previously deleted in the same transaction / persistence context - return null; - } - return (T) o.getBean(); - } - } - } - if (!query.isBeanCacheGet() || (t != null && t.isSkipCache())) { - return null; - } - // Hit the L2 bean cache - return desc.cacheBeanGet(id, query.isReadOnly(), pc); - } - /** * Return true if transactions PersistenceContext should be used. */ @@ -1023,15 +989,59 @@ public PersistenceContextScope persistenceContextScope(SpiQuery query) { @SuppressWarnings("unchecked") private T findId(SpiQuery query) { query.setType(Type.BEAN); + SpiOrmQueryRequest request = null; if (SpiQuery.Mode.NORMAL == query.mode() && !query.isForceHitDatabase()) { // See if we can skip doing the fetch completely by getting the bean from the // persistence context or the bean cache - T bean = findIdCheckPersistenceContextAndCache(query, query.getId()); - if (bean != null) { - return bean; + SpiTransaction t = (SpiTransaction) query.transaction(); + if (t == null) { + t = currentServerTransaction(); } + BeanDescriptor desc = query.descriptor(); + Object id = desc.convertId(query.getId()); + PersistenceContext pc = null; + if (t != null && useTransactionPersistenceContext(query)) { + // first look in the transaction scoped persistence context + pc = t.persistenceContext(); + if (pc != null) { + WithOption o = desc.contextGetWithOption(pc, id); + if (o != null) { + // We have found a hit. This could be also one with o.deleted() == true + // if bean was previously deleted in the same transaction / persistence context + return (T) o.getBean(); + } + } + } + if (t == null || !t.isSkipCache()) { + if (query.queryCacheMode() != CacheMode.OFF) { + request = buildQueryRequest(query); + if (request.isQueryCacheActive()) { + // Hit the query cache + request.prepareQuery(); + T bean = request.getFromQueryCache(); + if (bean != null) { + return bean; + } + } + } + if (query.isBeanCacheGet()) { + // Hit the L2 bean cache + T bean = desc.cacheBeanGet(id, query.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; + } + } + } + } + + if (request == null) { + request = buildQueryRequest(query); } - SpiOrmQueryRequest request = buildQueryRequest(query); request.prepareQuery(); if (request.isUseDocStore()) { return docStore().find(request); @@ -1077,15 +1087,18 @@ private T extractUnique(List list) { public Set findSet(SpiQuery query) { SpiOrmQueryRequest request = buildQueryRequest(Type.SET, query); request.resetBeanCacheAutoMode(false); + if (request.isQueryCacheActive()) { + request.prepareQuery(); + Object result = request.getFromQueryCache(); + if (result != null) { + return (Set) result; + } + } if (request.isGetAllFromBeanCache()) { // hit bean cache and got all results from cache return request.beanCacheHitsAsSet(); } request.prepareQuery(); - Object result = request.getFromQueryCache(); - if (result != null) { - return (Set) result; - } try { request.initTransIfRequired(); return request.findSet(); @@ -1098,16 +1111,19 @@ public Set findSet(SpiQuery query) { @SuppressWarnings({"unchecked", "rawtypes"}) public Map findMap(SpiQuery query) { SpiOrmQueryRequest request = buildQueryRequest(Type.MAP, query); + if (request.isQueryCacheActive()) { + request.prepareQuery(); + Object result = request.getFromQueryCache(); + if (result != null) { + return (Map) result; + } + } request.resetBeanCacheAutoMode(false); if (request.isGetAllFromBeanCache()) { // hit bean cache and got all results from cache return request.beanCacheHitsAsMap(); } request.prepareQuery(); - Object result = request.getFromQueryCache(); - if (result != null) { - return (Map) result; - } try { request.initTransIfRequired(); return request.findMap(); @@ -1413,15 +1429,18 @@ public List findList(SpiQuery query) { private List findList(SpiQuery query, boolean findOne) { SpiOrmQueryRequest request = buildQueryRequest(Type.LIST, query); request.resetBeanCacheAutoMode(findOne); + if (request.isQueryCacheActive()) { + request.prepareQuery(); + Object result = request.getFromQueryCache(); + if (result != null) { + return (List) result; + } + } if (request.isGetAllFromBeanCache()) { // hit bean cache and got all results from cache return request.beanCacheHits(); } request.prepareQuery(); - Object result = request.getFromQueryCache(); - if (result != null) { - return (List) result; - } if (request.isUseDocStore()) { return docStore().findList(request); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java index e804c1a171..08c0967f7c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/OrmQueryRequest.java @@ -39,10 +39,12 @@ public final class OrmQueryRequest extends BeanRequest implements SpiOrmQuery private PersistenceContext persistenceContext; private HashQuery cacheKey; private CQueryPlanKey queryPlanKey; + // The queryPlan during the request. + private CQueryPlan queryPlan; private SpiQuerySecondary secondaryQueries; private List cacheBeans; private boolean inlineCountDistinct; - private Set dependentTables; + private boolean prepared; private SpiQueryManyJoin manyJoin; public OrmQueryRequest(SpiEbeanServer server, OrmQueryEngine queryEngine, SpiQuery query, SpiTransaction t) { @@ -71,6 +73,7 @@ public boolean isDeleteByStatement() { } else { // delete by ids due to cascading delete needs queryPlanKey = query.setDeleteByIdsPlan(); + queryPlan = null; return false; } } @@ -168,11 +171,24 @@ private void adapterPreQuery() { */ @Override public void prepareQuery() { - manyJoin = query.convertJoins(); - secondaryQueries = query.secondaryQuery(); - beanDescriptor.prepareQuery(query); - adapterPreQuery(); - queryPlanKey = query.prepare(this); + if (!prepared) { + manyJoin = query.convertJoins(); + secondaryQueries = query.secondaryQuery(); + beanDescriptor.prepareQuery(query); + adapterPreQuery(); + queryPlanKey = query.prepare(this); + prepared = true; + } + } + + /** + * The queryPlanKey has to be updated, if elements are removed from an already prepared query. + */ + private void updateQueryPlanKey() { + if (prepared) { + queryPlanKey = query.prepare(this); + queryPlan = null; + } } public boolean isNativeSql() { @@ -467,7 +483,10 @@ public boolean includeManyJoin() { * query plan for this query exists. */ public CQueryPlan queryPlan() { - return beanDescriptor.queryPlan(queryPlanKey); + if (queryPlan == null) { + queryPlan = beanDescriptor.queryPlan(queryPlanKey); + } + return queryPlan; } /** @@ -485,6 +504,7 @@ public CQueryPlanKey queryPlanKey() { * Put the QueryPlan into the cache. */ public void putQueryPlan(CQueryPlan queryPlan) { + this.queryPlan = queryPlan; beanDescriptor.queryPlan(queryPlanKey, queryPlan); } @@ -493,8 +513,16 @@ public void resetBeanCacheAutoMode(boolean findOne) { query.resetBeanCacheAutoMode(findOne); } + @Override + public boolean isQueryCacheActive() { + return query.queryCacheMode() != CacheMode.OFF + && (transaction == null || !transaction.isSkipCache()) + && !server.isDisableL2Cache(); + } + + @Override public boolean isQueryCachePut() { - return cacheKey != null && query.queryCacheMode().isPut(); + return cacheKey != null && queryPlan != null && query.queryCacheMode().isPut(); } public boolean isBeanCachePutMany() { @@ -603,7 +631,14 @@ public boolean getFromBeanCache() { BeanCacheResult cacheResult = beanDescriptor.cacheIdLookup(persistenceContext, idLookup.idValues()); // adjust the query (IN clause) based on the cache hits this.cacheBeans = idLookup.removeHits(cacheResult); - return idLookup.allHits(); + if (idLookup.allHits()) { + return true; + } else { + if (!this.cacheBeans.isEmpty()) { + updateQueryPlanKey(); + } + return false; + } } if (!beanDescriptor.isNaturalKeyCaching()) { return false; @@ -616,7 +651,14 @@ public boolean getFromBeanCache() { BeanCacheResult cacheResult = beanDescriptor.naturalKeyLookup(persistenceContext, naturalKeySet.keys()); // adjust the query (IN clause) based on the cache hits this.cacheBeans = data.removeHits(cacheResult); - return data.allHits(); + if (data.allHits()) { + return true; + } else { + if (!this.cacheBeans.isEmpty()) { + updateQueryPlanKey(); + } + return false; + } } } return false; @@ -628,9 +670,7 @@ public boolean getFromBeanCache() { @Override @SuppressWarnings("unchecked") public Object getFromQueryCache() { - if (query.queryCacheMode() == CacheMode.OFF - || (transaction != null && transaction.isSkipCache()) - || server.isDisableL2Cache()) { + if (!isQueryCacheActive()) { return null; } else { cacheKey = query.queryHash(); @@ -684,8 +724,9 @@ private boolean readAuditQueryType() { } } + @Override public void putToQueryCache(Object result) { - beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, dependentTables, transaction.startNanoTime())); + beanDescriptor.queryCachePut(cacheKey, new QueryCacheEntry(result, queryPlan.dependentTables(), transaction.startNanoTime())); } /** @@ -755,15 +796,6 @@ public boolean isInlineCountDistinct() { return inlineCountDistinct; } - public void addDependentTables(Set tables) { - if (tables != null && !tables.isEmpty()) { - if (dependentTables == null) { - dependentTables = new LinkedHashSet<>(); - } - dependentTables.addAll(tables); - } - } - /** * Return true if no MaxRows or use LIMIT in SQL update. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java index e81da64f54..3361b87414 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/SpiOrmQueryRequest.java @@ -135,6 +135,21 @@ public interface SpiOrmQueryRequest extends BeanQueryRequest, DocQueryRequ */ A getFromQueryCache(); + /** + * Return if query cache is active. + */ + boolean isQueryCacheActive(); + + /** + * Return if results should be put to query cache. + */ + boolean isQueryCachePut(); + + /** + * Put the result to the query cache. + */ + void putToQueryCache(Object result); + /** * Maybe hit the bean cache returning true if everything was obtained from the * cache (that there were no misses). diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java index 85d6319e9d..f8cc26aaa4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQuery.java @@ -754,7 +754,4 @@ public void handleLoadError(String fullName, Exception e) { query.handleLoadError(fullName, e); } - public Set dependentTables() { - return queryPlan.dependentTables(); - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java index 2a51dd515e..3d4d7e4c3a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryEngine.java @@ -103,7 +103,6 @@ private > A findAttributeCollection(OrmQueryRequest r request.transaction().logSummary(rcQuery.summary()); } if (request.isQueryCachePut()) { - request.addDependentTables(rcQuery.dependentTables()); if (collection instanceof List) { collection = (A) Collections.unmodifiableList((List) collection); request.putToQueryCache(collection); @@ -167,7 +166,6 @@ public int findCount(OrmQueryRequest request) { request.transaction().logSummary(rcQuery.summary()); } if (request.isQueryCachePut()) { - request.addDependentTables(rcQuery.dependentTables()); request.putToQueryCache(count); } return count; @@ -355,9 +353,6 @@ BeanCollection findMany(OrmQueryRequest request) { cquery.auditFindMany(); } request.executeSecondaryQueries(false); - if (request.isQueryCachePut()) { - request.addDependentTables(cquery.dependentTables()); - } return beanCollection; } catch (SQLException e) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java index 1f55bf2924..c347142958 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryFetchSingleAttribute.java @@ -168,10 +168,6 @@ public void profile() { .addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId()); } - Set dependentTables() { - return queryPlan.dependentTables(); - } - @Override public void cancel() { lock.lock(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java index 56cce175e7..7fdf3d99b9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryRowCount.java @@ -140,10 +140,6 @@ public void profile() { .addQueryEvent(query.profileEventId(), profileOffset, desc.name(), rowCount, query.profileId()); } - Set dependentTables() { - return queryPlan.dependentTables(); - } - @Override public void cancel() { lock.lock(); 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 3679950862..d896010c04 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 @@ -164,8 +164,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/basic/TestDeleteByIdCollection.java b/ebean-test/src/test/java/org/tests/basic/TestDeleteByIdCollection.java index 30f2239dee..d2737e89a3 100644 --- a/ebean-test/src/test/java/org/tests/basic/TestDeleteByIdCollection.java +++ b/ebean-test/src/test/java/org/tests/basic/TestDeleteByIdCollection.java @@ -1,6 +1,7 @@ package org.tests.basic; import io.ebean.DB; +import io.ebean.test.LoggedSql; import io.ebean.xtest.base.TransactionalTestCase; import org.junit.jupiter.api.Test; import org.tests.model.basic.Customer; @@ -10,6 +11,7 @@ import java.util.ArrayList; import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -41,8 +43,10 @@ public void test() { DB.deleteAll(Customer.class, ids); awaitL2Cache(); + LoggedSql.start(); c0Back = DB.find(Customer.class, c0.getId()); c1Back = DB.find(Customer.class, "" + c1.getId()); + assertThat(LoggedSql.stop()).isEmpty(); assertNull(c0Back); assertNull(c1Back); diff --git a/ebean-test/src/test/java/org/tests/cache/TestBeanCache.java b/ebean-test/src/test/java/org/tests/cache/TestBeanCache.java index 0f4a3b423c..fab2070c6f 100644 --- a/ebean-test/src/test/java/org/tests/cache/TestBeanCache.java +++ b/ebean-test/src/test/java/org/tests/cache/TestBeanCache.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -67,7 +68,7 @@ public void idsInFindMap() { // Test findIds LoggedSql.start(); - query.copy() + Map map1 = query.copy() .where().idIn(ids.subList(0, 1)) .findMap(); // cache key is: 3/d[{/c1000}]/w[List[IdIn[?1],]] if (isPostgresCompatible()) { @@ -77,7 +78,7 @@ public void idsInFindMap() { } LoggedSql.start(); - query.copy() + Map map2 = query.copy() .where().idIn(ids.subList(0, 4)) .findMap(); // cache key is: 3/d[{/c1000}]/w[List[IdIn[?5],]] if (isPostgresCompatible()) { @@ -87,7 +88,7 @@ public void idsInFindMap() { } LoggedSql.start(); - query.copy() + Map map3 = query.copy() .where().idIn(ids.subList(2, 6)) .findMap(); // same cache key as above and same SQL above if (isPostgresCompatible()) { @@ -95,6 +96,112 @@ public void idsInFindMap() { } else { assertThat(LoggedSql.stop().get(0)).contains("in (?,?,?,?,?)"); } + + ServerCache bc = DB.getDefault().pluginApi().cacheManager().beanCache(OCachedBean.class); + bc.statistics(true); + + ServerCache qc = DB.getDefault().pluginApi().cacheManager().queryCache(OCachedBean.class); + qc.statistics(true); + + LoggedSql.start(); + assertThat(query.copy() + .where().idIn(ids.subList(0, 1)) + .findMap()).isEqualTo(map1).isNotSameAs(map1); + + + assertThat(query.copy() + .where().idIn(ids.subList(0, 4)) + .findMap()).isEqualTo(map2).isNotSameAs(map2); + + assertThat(query.copy() + .where().idIn(ids.subList(2, 6)) + .findMap()).isEqualTo(map3).isNotSameAs(map3); + + assertThat(LoggedSql.stop()).isEmpty(); // we should have no DB-hits + + // we should have all beans in the bean cache, but no hits in the query cache + assertThat(bc.statistics(true).getHitCount()).isEqualTo(map1.size() + map2.size() + map3.size()); + assertThat(qc.statistics(true).getHitCount()).isEqualTo(0); + + // check with a different set, that should be in the cache + query.copy().where().idIn(ids.subList(2, 5)).findMap(); + assertThat(bc.statistics(true).getHitCount()).isEqualTo(3); + assertThat(qc.statistics(true).getHitCount()).isEqualTo(0); + + } + + + @Test + public void idsInFindMapWithBeanAndQueryCache() { + + List beans = createBeans(Arrays.asList("m0", "m1", "m2", "m3", "m4", "m5", "m6")); + List ids = beans.stream().map(OCachedBean::getId).collect(Collectors.toList()); + beanCache.clear(); + beanCache.statistics(true); + Query query = DB.find(OCachedBean.class) + .setUseCache(true) + .setUseQueryCache(true) + .setReadOnly(true); + + // Test findIds + LoggedSql.start(); + Map map1 = query.copy() + .where().idIn(ids.subList(0, 1)) + .findMap(); // cache key is: 3/d[{/c1000}]/w[List[IdIn[?1],]] + if (isPostgresCompatible()) { + assertThat(LoggedSql.stop().get(0)).contains("t0.id = any(?)"); + } else { + assertThat(LoggedSql.stop().get(0)).contains("in (?)"); + } + + LoggedSql.start(); + Map map2 = query.copy() + .where().idIn(ids.subList(0, 4)) + .findMap(); // cache key is: 3/d[{/c1000}]/w[List[IdIn[?5],]] + if (isPostgresCompatible()) { + assertThat(LoggedSql.stop().get(0)).contains("t0.id = any(?)"); + } else { + assertThat(LoggedSql.stop().get(0)).contains("in (?,?,?,?,?)"); + } + + LoggedSql.start(); + Map map3 = query.copy() + .where().idIn(ids.subList(2, 6)) + .findMap(); // same cache key as above and same SQL above + if (isPostgresCompatible()) { + assertThat(LoggedSql.stop().get(0)).contains("t0.id = any(?)"); + } else { + assertThat(LoggedSql.stop().get(0)).contains("in (?,?,?,?,?)"); + } + + ServerCache bc = DB.getDefault().pluginApi().cacheManager().beanCache(OCachedBean.class); + bc.statistics(true); + + ServerCache qc = DB.getDefault().pluginApi().cacheManager().queryCache(OCachedBean.class); + qc.statistics(true); + + LoggedSql.start(); + assertThat(query.copy() + .where().idIn(ids.subList(0, 1)) + .findMap()).isEqualTo(map1).isSameAs(map1); + + assertThat(query.copy() + .where().idIn(ids.subList(0, 4)) + .findMap()).isEqualTo(map2).isSameAs(map2); + + assertThat(query.copy() + .where().idIn(ids.subList(2, 6)) + .findMap()).isEqualTo(map3).isSameAs(map3); + + assertThat(LoggedSql.stop()).isEmpty(); // we should have no DB-hits + + assertThat(bc.statistics(true).getHitCount()).isEqualTo(0); + assertThat(qc.statistics(true).getHitCount()).isEqualTo(3); + + // check with a different set, that should be in the cache + query.copy().where().idIn(ids.subList(2, 5)).findMap(); + assertThat(bc.statistics(true).getHitCount()).isEqualTo(3); + assertThat(qc.statistics(true).getHitCount()).isEqualTo(0); } @Test 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() { diff --git a/ebean-test/src/test/java/org/tests/model/basic/OCachedBean.java b/ebean-test/src/test/java/org/tests/model/basic/OCachedBean.java index 649a0c0072..bb906f8e8d 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/OCachedBean.java +++ b/ebean-test/src/test/java/org/tests/model/basic/OCachedBean.java @@ -9,7 +9,7 @@ /** * Cached bean for testing caching implementation. */ -@Cache +@Cache(enableQueryCache = true) @Entity @Table(name = "o_cached_bean") public class OCachedBean { From 869d48957049d5d073a7c3383235999e1229cf64 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 16 Jan 2023 16:00:12 +0100 Subject: [PATCH 03/36] NEW: Feature to disable overwriting generated properties --- .../src/main/java/io/ebean/Transaction.java | 6 +++ .../io/ebeaninternal/api/SpiTransaction.java | 5 +++ .../api/SpiTransactionProxy.java | 10 +++++ .../server/core/PersistRequestBean.java | 20 ++++++--- .../ImplicitReadOnlyTransaction.java | 9 ++++ .../server/transaction/JdbcTransaction.java | 11 +++++ .../server/transaction/NoTransaction.java | 9 ++++ .../generated/TestGeneratedProperties.java | 43 ++++++++++++++++++- 8 files changed, 105 insertions(+), 8 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/Transaction.java b/ebean-api/src/main/java/io/ebean/Transaction.java index 4f7c148189..adaadf00c1 100644 --- a/ebean-api/src/main/java/io/ebean/Transaction.java +++ b/ebean-api/src/main/java/io/ebean/Transaction.java @@ -260,6 +260,12 @@ static Transaction current() { */ void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties); + /** + * If set to false (default is true) generated propertes are only set, if it is the version property or have a null value. + * This may be useful in backup & restore scenarios, if you want set WhenCreated/WhenModified. + */ + void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties); + /** * Set if the L2 cache should be skipped for "find by id" and "find by natural key" queries. *

diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java index 0e5df22511..2f1605d8d3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransaction.java @@ -103,6 +103,11 @@ public interface SpiTransaction extends Transaction { */ Boolean isUpdateAllLoadedProperties(); + /** + * Returns true, if generated properties are overwritten (default) or are only set, if they are null. + */ + boolean isOverwriteGeneratedProperties(); + /** * Return the batchSize specifically set for this transaction or 0. *

diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java index 9c2057a4a5..7b79a3f5c4 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java @@ -248,6 +248,16 @@ public void setUpdateAllLoadedProperties(boolean updateAllLoaded) { transaction.setUpdateAllLoadedProperties(updateAllLoaded); } + @Override + public void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties) { + transaction.setOverwriteGeneratedProperties(overwriteGeneratedProperties); + } + + @Override + public boolean isOverwriteGeneratedProperties() { + return transaction.isOverwriteGeneratedProperties(); + } + @Override public Boolean isUpdateAllLoadedProperties() { return transaction.isUpdateAllLoadedProperties(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java index 93358d0c6d..6286da2e0f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/PersistRequestBean.java @@ -275,24 +275,30 @@ private void onUpdateGeneratedProperties() { } else { // @WhenModified set without invoking interception Object oldVal = prop.getValue(entityBean); - Object value = generatedProperty.getUpdateValue(prop, entityBean, now()); - prop.setValueChanged(entityBean, value); - intercept.setOldValue(prop.propertyIndex(), oldVal); + if (transaction == null || transaction.isOverwriteGeneratedProperties() || oldVal == null) { // version handled above + Object value = generatedProperty.getUpdateValue(prop, entityBean, now()); + prop.setValueChanged(entityBean, value); + intercept.setOldValue(prop.propertyIndex(), oldVal); + } } } } private void onFailedUpdateUndoGeneratedProperties() { for (BeanProperty prop : beanDescriptor.propertiesGenUpdate()) { - Object oldVal = intercept.origValue(prop.propertyIndex()); - prop.setValue(entityBean, oldVal); + if (transaction == null || transaction.isOverwriteGeneratedProperties() || prop.isVersion()) { + Object oldVal = intercept.origValue(prop.propertyIndex()); + prop.setValue(entityBean, oldVal); + } } } private void onInsertGeneratedProperties() { for (BeanProperty prop : beanDescriptor.propertiesGenInsert()) { - Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now()); - prop.setValueChanged(entityBean, value); + if (transaction == null || transaction.isOverwriteGeneratedProperties() || prop.isVersion() || prop.getValue(entityBean) == null) { + Object value = prop.generatedProperty().getInsertValue(prop, entityBean, now()); + prop.setValueChanged(entityBean, value); + } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java index 1353a217db..5573fc6d79 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/ImplicitReadOnlyTransaction.java @@ -272,6 +272,15 @@ public void setReadOnly(boolean readOnly) { public void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties) { } + @Override + public void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties) { + } + + @Override + public boolean isOverwriteGeneratedProperties() { + return true; + } + @Override public Boolean isUpdateAllLoadedProperties() { return null; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java index 14987e8355..b591765846 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/JdbcTransaction.java @@ -53,6 +53,7 @@ class JdbcTransaction implements SpiTransaction, TxnProfileEventCodes { private boolean queryOnly = true; private boolean localReadOnly; private Boolean updateAllLoadedProperties; + private boolean overwriteGeneratedProperties = true; private boolean oldBatchMode; private boolean batchMode; private boolean batchOnCascadeMode; @@ -452,6 +453,16 @@ public final Boolean isUpdateAllLoadedProperties() { return updateAllLoadedProperties; } + @Override + public void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties) { + this.overwriteGeneratedProperties = overwriteGeneratedProperties; + } + + @Override + public boolean isOverwriteGeneratedProperties() { + return overwriteGeneratedProperties; + } + @Override public final void setBatchMode(boolean batchMode) { this.batchMode = batchMode; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java index c18633a79a..e63e50206f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/NoTransaction.java @@ -230,6 +230,15 @@ public void setPersistCascade(boolean persistCascade) { public void setUpdateAllLoadedProperties(boolean updateAllLoadedProperties) { } + @Override + public void setOverwriteGeneratedProperties(boolean overwriteGeneratedProperties) { + } + + @Override + public boolean isOverwriteGeneratedProperties() { + return true; + } + @Override public void setSkipCache(boolean skipCache) { } diff --git a/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java b/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java index ced209f8bc..ee97cc1467 100644 --- a/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java +++ b/ebean-test/src/test/java/org/tests/generated/TestGeneratedProperties.java @@ -1,10 +1,13 @@ package org.tests.generated; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; +import io.ebean.Transaction; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.Test; import org.tests.model.EGenProps; +import java.time.Instant; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -47,4 +50,42 @@ public void test_insert() { DB.delete(bean); } + + @Test + public void test_update_no_overwrite() { + EGenProps bean = new EGenProps(); + bean.setName("updating"); + DB.save(bean); + + bean = DB.find(EGenProps.class, bean.getId()); + bean.setInstantCreated(Instant.parse("2022-01-01T00:00:00Z")); + bean.setInstantUpdated(Instant.parse("2022-01-02T00:00:00Z")); + try (Transaction txn = DB.beginTransaction()) { + txn.setOverwriteGeneratedProperties(false); + DB.save(bean); + txn.commit(); + } + + bean = DB.find(EGenProps.class, bean.getId()); + assertThat(bean.getInstantCreated()).isEqualTo(Instant.parse("2022-01-01T00:00:00Z")); + assertThat(bean.getInstantUpdated()).isEqualTo(Instant.parse("2022-01-02T00:00:00Z")); + } + + @Test + public void test_insert_no_overwrite() { + EGenProps bean = new EGenProps(); + try (Transaction txn = DB.beginTransaction()) { + txn.setOverwriteGeneratedProperties(false); + bean.setName("inserting"); + bean.setInstantCreated(Instant.parse("2022-01-01T00:00:00Z")); + bean.setInstantUpdated(Instant.parse("2022-01-02T00:00:00Z")); + DB.save(bean); + txn.commit(); + } + + + bean = DB.find(EGenProps.class, bean.getId()); + assertThat(bean.getInstantCreated()).isEqualTo(Instant.parse("2022-01-01T00:00:00Z")); + assertThat(bean.getInstantUpdated()).isEqualTo(Instant.parse("2022-01-02T00:00:00Z")); + } } From 95abfd692022a2c0a5a6e01fa3cc9b80362399ab Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 14:51:23 +0200 Subject: [PATCH 04/36] Support for lazy add on BeanList --- .../java/io/ebean/bean/BeanCollection.java | 7 + .../main/java/io/ebean/common/BeanList.java | 26 +-- .../java/io/ebean/common/BeanListLazyAdd.java | 133 +++++++++++++ .../server/deploy/BaseCollectionHelp.java | 8 +- .../server/deploy/BeanCollectionUtil.java | 2 +- .../server/deploy/BeanListHelp.java | 22 ++- .../server/deploy/BeanMapHelp.java | 7 +- .../server/deploy/BeanSetHelp.java | 7 - .../tests/batchload/TestLazyAddBeanList.java | 181 ++++++++++++++++++ 9 files changed, 353 insertions(+), 40 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java create mode 100644 ebean-test/src/test/java/org/tests/batchload/TestLazyAddBeanList.java diff --git a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java index 1161476ddd..4c07e9da45 100644 --- a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java +++ b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java @@ -164,6 +164,13 @@ enum ModifyListenMode { */ Collection actualEntries(); + /** + * Returns entries, that were lazily added at the end of the list. Might be null. + */ + default Collection getLazyAddedEntries(boolean reset) { + return null; + } + /** * return true if there are real rows held. Return false is this is using * Deferred fetch to lazy load the rows and the rows have not yet been diff --git a/ebean-api/src/main/java/io/ebean/common/BeanList.java b/ebean-api/src/main/java/io/ebean/common/BeanList.java index 12d0770d62..a1ecb1d4ec 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanList.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanList.java @@ -8,14 +8,14 @@ /** * List capable of lazy loading and modification awareness. */ -public final class BeanList extends AbstractBeanCollection implements List, BeanCollectionAdd { +public class BeanList extends AbstractBeanCollection implements List, BeanCollectionAdd { private static final long serialVersionUID = 1L; /** * The underlying List implementation. */ - private List list; + List list; /** * Specify the underlying List implementation. @@ -111,30 +111,30 @@ public boolean checkEmptyLazyLoad() { } } + protected void initList(boolean skipLoad, boolean onlyIds) { + if (skipLoad) { + list = new ArrayList<>(); + } else { + lazyLoadCollection(onlyIds); + } + } + private void initClear() { lock.lock(); try { if (list == null) { - if (!disableLazyLoad && modifyListening) { - lazyLoadCollection(true); - } else { - list = new ArrayList<>(); - } + initList(disableLazyLoad || !modifyListening, true); } } finally { lock.unlock(); } } - private void init() { + void init() { lock.lock(); try { if (list == null) { - if (disableLazyLoad) { - list = new ArrayList<>(); - } else { - lazyLoadCollection(false); - } + initList(disableLazyLoad, false); } } finally { lock.unlock(); diff --git a/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java b/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java new file mode 100644 index 0000000000..8ffa4b47df --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/common/BeanListLazyAdd.java @@ -0,0 +1,133 @@ +package io.ebean.common; + +import io.ebean.bean.BeanCollection; +import io.ebean.bean.BeanCollectionLoader; +import io.ebean.bean.EntityBean; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * This bean list can perform additions without populating the list. + * This might be useful, if you just want to add entries to an existing collection. + * Works only for lists and only if there is no order column + */ +public class BeanListLazyAdd extends BeanList { + + public BeanListLazyAdd(BeanCollectionLoader loader, EntityBean ownerBean, String propertyName) { + super(loader, ownerBean, propertyName); + } + + private List lazyAddedEntries; + + @Override + public boolean add(E bean) { + checkReadOnly(); + + lock.lock(); + try { + if (list == null) { + // list is not yet initialized, so we may add elements to a spare list + if (lazyAddedEntries == null) { + lazyAddedEntries = new ArrayList<>(); + } + lazyAddedEntries.add(bean); + } else { + list.add(bean); + } + } finally { + lock.unlock(); + } + + if (modifyListening) { + modifyAddition(bean); + } + return true; + } + + @Override + public boolean addAll(Collection beans) { + checkReadOnly(); + + lock.lock(); + try { + if (list == null) { + // list is not yet initialized, so we may add elements to a spare list + if (lazyAddedEntries == null) { + lazyAddedEntries = new ArrayList<>(); + } + lazyAddedEntries.addAll(beans); + } else { + list.addAll(beans); + } + } finally { + lock.unlock(); + } + + if (modifyListening) { + getModifyHolder().modifyAdditionAll(beans); + } + + return true; + } + + + @Override + public void loadFrom(BeanCollection other) { + super.loadFrom(other); + if (lazyAddedEntries != null) { + list.addAll(lazyAddedEntries); + lazyAddedEntries = null; + } + } + + /** + * on init, this happens on all accessor methods except on 'add' and addAll, + * we add the lazy added entries at the end of the list + */ + @Override + protected void initList(boolean skipLoad, boolean onlyIds) { + if (skipLoad) { + if (lazyAddedEntries != null) { + list = lazyAddedEntries; + lazyAddedEntries = null; + } else { + list = new ArrayList<>(); + } + } else { + lazyLoadCollection(onlyIds); + if (lazyAddedEntries != null) { + list.addAll(lazyAddedEntries); + lazyAddedEntries = null; + } + } + } + + @Override + public List getLazyAddedEntries(boolean reset) { + List ret = lazyAddedEntries; + if (reset) { + lazyAddedEntries = null; + } + return ret; + } + + @Override + public boolean isSkipSave() { + return lazyAddedEntries == null && super.isSkipSave(); + } + + public boolean checkEmptyLazyLoad() { + if (list != null) { + return false; + } else if (lazyAddedEntries == null) { + list = new ArrayList<>(); + return true; + } else { + list = lazyAddedEntries; + lazyAddedEntries = null; + return false; + } + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java index b6cb6314ba..f161f18230 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BaseCollectionHelp.java @@ -11,7 +11,7 @@ abstract class BaseCollectionHelp implements BeanCollectionHelp { final BeanPropertyAssocMany many; - private final BeanDescriptor targetDescriptor; + final BeanDescriptor targetDescriptor; final String propertyName; BeanCollectionLoader loader; @@ -21,12 +21,6 @@ abstract class BaseCollectionHelp implements BeanCollectionHelp { this.propertyName = many.name(); } - BaseCollectionHelp() { - this.many = null; - this.targetDescriptor = null; - this.propertyName = null; - } - @Override public final void setLoader(BeanCollectionLoader loader) { this.loader = loader; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java index e21ab53f61..72abcd3359 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanCollectionUtil.java @@ -32,7 +32,7 @@ public static Collection getActualEntries(Object o) { if (o instanceof BeanCollection) { BeanCollection bc = (BeanCollection) o; if (!bc.isPopulated()) { - return null; + return bc.getLazyAddedEntries(true); } // For maps this is a collection of Map.Entry, otherwise it // returns a collection of beans diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanListHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanListHelp.java index dc4a8c520f..a611204bdc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanListHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanListHelp.java @@ -5,6 +5,7 @@ import io.ebean.bean.BeanCollectionAdd; import io.ebean.bean.EntityBean; import io.ebean.common.BeanList; +import io.ebean.common.BeanListLazyAdd; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.api.json.SpiJsonWriter; @@ -19,12 +20,11 @@ */ public class BeanListHelp extends BaseCollectionHelp { + private final boolean hasOrderColumn; + BeanListHelp(BeanPropertyAssocMany many) { super(many); - } - - BeanListHelp() { - super(); + hasOrderColumn = many.hasOrderColumn(); } @Override @@ -53,7 +53,12 @@ public final BeanCollection createEmptyNoParent() { @Override public final BeanCollection createEmpty(EntityBean parentBean) { - BeanList beanList = new BeanList<>(loader, parentBean, propertyName); + BeanList beanList; + if (hasOrderColumn) { + beanList = new BeanList<>(loader, parentBean, propertyName); + } else { + beanList = new BeanListLazyAdd<>(loader, parentBean, propertyName); + } if (many != null) { beanList.setModifyListening(many.modifyListenMode()); } @@ -62,7 +67,12 @@ public final BeanCollection createEmpty(EntityBean parentBean) { @Override public final BeanCollection createReference(EntityBean parentBean) { - BeanList beanList = new BeanList<>(loader, parentBean, propertyName); + BeanList beanList; + if (hasOrderColumn) { + beanList = new BeanList<>(loader, parentBean, propertyName); + } else { + beanList = new BeanListLazyAdd<>(loader, parentBean, propertyName); + } beanList.setModifyListening(many.modifyListenMode()); return beanList; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java index 041a6ab7d7..ba8774912a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMapHelp.java @@ -20,18 +20,13 @@ */ public class BeanMapHelp extends BaseCollectionHelp { - private final BeanPropertyAssocMany many; - private final BeanDescriptor targetDescriptor; - private final String propertyName; private final BeanProperty beanProperty; /** * When help is attached to a specific many property. */ BeanMapHelp(BeanPropertyAssocMany many) { - this.many = many; - this.targetDescriptor = many.targetDescriptor(); - this.propertyName = many.name(); + super(many); this.beanProperty = targetDescriptor.beanProperty(many.mapKey()); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanSetHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanSetHelp.java index da390bf174..474d4f74b7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanSetHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanSetHelp.java @@ -26,13 +26,6 @@ public class BeanSetHelp extends BaseCollectionHelp { super(many); } - /** - * For a query that returns a set. - */ - BeanSetHelp() { - super(); - } - @Override public final BeanCollectionAdd getBeanCollectionAdd(Object bc, String mapKey) { if (bc instanceof BeanSet) { diff --git a/ebean-test/src/test/java/org/tests/batchload/TestLazyAddBeanList.java b/ebean-test/src/test/java/org/tests/batchload/TestLazyAddBeanList.java new file mode 100644 index 0000000000..31233cf700 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/batchload/TestLazyAddBeanList.java @@ -0,0 +1,181 @@ +package org.tests.batchload; + +import io.ebean.DB; +import io.ebean.common.BeanList; +import io.ebean.common.BeanListLazyAdd; +import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.tests.model.basic.Contact; +import org.tests.model.basic.Customer; +import org.tests.order.OrderMaster; +import org.tests.order.OrderReferencedChild; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestLazyAddBeanList extends BaseTestCase { + + private Customer cust; + private OrderMaster orderMaster; + + @BeforeEach + void init() { + cust = new Customer(); + cust.setName("noFetch"); + DB.save(cust); + cust = DB.find(Customer.class, cust.getId()); // fetch fresh from DB + + orderMaster = new OrderMaster(); + DB.save(orderMaster); + orderMaster = DB.find(OrderMaster.class, orderMaster.getId()); + } + + @AfterEach + void tearDown() { + DB.delete(cust); + DB.delete(orderMaster); + } + + @Test + public void testCollectionType() { + assertThat(cust.getContacts()) + .isInstanceOf(BeanListLazyAdd.class); + + assertThat(orderMaster.getChildren()) + .isInstanceOf(BeanList.class) + .isNotInstanceOf(BeanListLazyAdd.class); + + assertThat(DB.reference(Customer.class, 1).getContacts()) + .isInstanceOf(BeanListLazyAdd.class); + + assertThat(DB.reference(OrderMaster.class, 1).getChildren()) + .isInstanceOf(BeanList.class) + .isNotInstanceOf(BeanListLazyAdd.class); + } + + @Test + public void testNoFetch() { + + LoggedSql.start(); + cust.addContact(new Contact("jim", "slim")); + cust.addContact(new Contact("joe", "big")); + DB.save(cust); + + List sql = LoggedSql.stop(); + assertThat(sql.get(0)).contains("insert into contact"); + assertThat(sql.get(1)).contains("bind").contains("jim,slim,"); + assertThat(sql.get(2)).contains("bind").contains("joe,big,"); + assertThat(sql.get(3)).contains("executeBatch() size:2").contains("sql:insert into contact"); + assertThat(sql).hasSize(4); + + assertThat(cust.getContacts()).hasSize(2); + + List list = DB.find(Customer.class) + .fetch("contacts", "firstName") + .where() + .idEq(cust.getId()).findSingleAttributeList(); +// check if it is really saved + assertThat(list).containsExactlyInAnyOrder("jim", "joe"); + } + + @Test + public void testFetch() { + + LoggedSql.start(); + orderMaster.getChildren().add(new OrderReferencedChild("foo")); + orderMaster.getChildren().add(new OrderReferencedChild("bar")); + DB.save(orderMaster); + + List sql = LoggedSql.stop(); + + assertThat(sql.get(0)).contains("from order_referenced_parent"); // lazy load children + assertThat(sql.get(1)).contains("insert into order_referenced_parent"); + assertThat(sql.get(2)).contains("bind").contains("D,foo,"); + assertThat(sql.get(3)).contains("bind").contains("D,bar,"); + + List list = DB.find(OrderMaster.class) + .fetch("children", "name") + .where().idEq(orderMaster.getId()) + .findSingleAttributeList(); + // check if it is really saved + assertThat(list).containsExactlyInAnyOrder("foo", "bar"); + } + + @Test + public void testAddToExisting() { + + // add some existing entries + LoggedSql.start(); + cust.getContacts().addAll(Arrays.asList( + new Contact("jim", "slim"), + new Contact("joe", "big"))); + assertThat(LoggedSql.stop()).isEmpty(); + + LoggedSql.start(); + DB.save(cust); + assertThat(LoggedSql.stop()).hasSize(4); // insert + 2x bind + executeBatch() + + cust = DB.find(Customer.class, cust.getId()); + + LoggedSql.start(); + List contacts = cust.getContacts(); + contacts.add(new Contact("charlie", "brown")); + assertThat(LoggedSql.stop()).isEmpty(); + + LoggedSql.start(); + assertThat(contacts). + extracting(Contact::getFirstName). + containsExactlyInAnyOrder("jim", "joe", "charlie"); + List sql = LoggedSql.stop(); + + assertThat(sql.get(0)).contains("from o_customer t0 left join contact t1 "); + assertThat(sql).hasSize(1); + } + + @Test + public void testBatch() { + for (int i = 0; i < 10; i++) { + Customer bcust = new Customer(); + bcust.setName("batch " + i); + //bcust.getContacts().add(new Contact("Noemi","Praml")); + DB.save(bcust); + } + + LoggedSql.start(); + List custs = DB.find(Customer.class).where().startsWith("name", "batch").findList(); + assertThat(custs).hasSize(10); + assertThat(LoggedSql.stop()).hasSize(1); + + + LoggedSql.start(); + for (Customer cust : custs) { + cust.getContacts().addAll(Arrays.asList( + new Contact(cust.getName() + " jim", "slim"), + new Contact(cust.getName() + " joe", "big"))); + } + assertThat(LoggedSql.stop()).isEmpty(); + + LoggedSql.start(); + custs.get(0).getContacts().size(); // trigger batch load + assertThat(LoggedSql.stop()).hasSize(1); + + LoggedSql.start(); + DB.saveAll(custs); + assertThat(LoggedSql.stop()).hasSize(22); + + LoggedSql.start(); + custs = DB.find(Customer.class).where().startsWith("name", "batch").findList(); + assertThat(custs).hasSize(10); + + for (Customer cust : custs) { + assertThat(cust.getContacts()).hasSize(2).extracting(Contact::getFirstName).containsExactlyInAnyOrder(cust.getName() + " jim", cust.getName() + " joe"); + } + assertThat(LoggedSql.stop()).hasSize(2); + } + +} From 0648235c4888e9c826f3118f7b50c47c9cc2da1b Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 15:06:55 +0200 Subject: [PATCH 05/36] Tenant support for query plans --- .../java/io/ebean/meta/MetaQueryPlan.java | 5 +++ .../io/ebeaninternal/api/SpiDbQueryPlan.java | 2 +- .../api/SpiTransactionManager.java | 2 +- .../server/core/InternalConfiguration.java | 3 +- .../server/query/CQueryBindCapture.java | 29 +++++++++---- .../server/query/CQueryPlanManager.java | 19 ++++----- .../server/query/CQueryPlanRequest.java | 18 +++----- .../server/query/DQueryPlanOutput.java | 23 ++++++++++- .../transaction/TransactionManager.java | 4 +- .../partition/MultiTenantPartitionTest.java | 41 ++++++++++++++++++- 10 files changed, 109 insertions(+), 37 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java b/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java index 7c431022c5..4258b7c9ca 100644 --- a/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java +++ b/ebean-api/src/main/java/io/ebean/meta/MetaQueryPlan.java @@ -44,6 +44,11 @@ public interface MetaQueryPlan { */ String plan(); + /** + * The tenant ID of the plan. + */ + Object tenantId(); + /** * Return the query execution time associated with the bind values capture. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java index 535232d76a..c8a4210055 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDbQueryPlan.java @@ -12,6 +12,6 @@ public interface SpiDbQueryPlan extends MetaQueryPlan { /** * Extend with queryTimeMicros, captureCount, captureMicros and when the bind values were captured. */ - SpiDbQueryPlan with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured); + SpiDbQueryPlan with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured, Object tenantId); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java index 01311d1673..cc0ff69efa 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionManager.java @@ -60,6 +60,6 @@ public interface SpiTransactionManager { /** * Return a connection used for query plan collection. */ - Connection queryPlanConnection() throws SQLException; + Connection queryPlanConnection(Object tenantId) throws SQLException; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index c6b6717417..ecbb7f29e2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -583,7 +583,8 @@ public QueryPlanManager initQueryPlanManager(TransactionManager transactionManag return QueryPlanManager.NOOP; } long threshold = config.getQueryPlanThresholdMicros(); - return new CQueryPlanManager(transactionManager, threshold, queryPlanLogger(databasePlatform.platform()), extraMetrics); + return new CQueryPlanManager(transactionManager, config.getCurrentTenantProvider(), + threshold, queryPlanLogger(databasePlatform.platform()), extraMetrics); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java index 855d7f0104..6d57148f5d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryBindCapture.java @@ -1,14 +1,17 @@ package io.ebeaninternal.server.query; -import io.ebeaninternal.api.SpiDbQueryPlan; -import io.ebeaninternal.api.SpiQueryBindCapture; -import io.ebeaninternal.api.SpiQueryPlan; +import io.ebean.config.CurrentTenantProvider; +import io.ebeaninternal.api.*; import io.ebeaninternal.server.bind.capture.BindCapture; +import java.sql.Connection; +import java.sql.SQLException; import java.time.Instant; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; +import static java.lang.System.Logger.Level.ERROR; + final class CQueryBindCapture implements SpiQueryBindCapture { private static final double multiplier = 1.5d; @@ -16,18 +19,21 @@ final class CQueryBindCapture implements SpiQueryBindCapture { private final ReentrantLock lock = new ReentrantLock(); private final CQueryPlanManager manager; private final SpiQueryPlan queryPlan; + private final CurrentTenantProvider tenantProvider; private BindCapture bindCapture; private long queryTimeMicros; private long thresholdMicros; private long captureCount; + private Object tenantId; private long lastBindCapture; - CQueryBindCapture(CQueryPlanManager manager, SpiQueryPlan queryPlan, long thresholdMicros) { + CQueryBindCapture(CQueryPlanManager manager, SpiQueryPlan queryPlan, long thresholdMicros, CurrentTenantProvider tenantProvider) { this.manager = manager; this.queryPlan = queryPlan; this.thresholdMicros = thresholdMicros; + this.tenantProvider = tenantProvider; } /** @@ -45,6 +51,7 @@ public void setBind(BindCapture bindCapture, long queryTimeMicros, long startNan this.thresholdMicros = Math.round(queryTimeMicros * multiplier); this.captureCount++; this.bindCapture = bindCapture; + this.tenantId = tenantProvider == null ? null : tenantProvider.currentId(); this.queryTimeMicros = queryTimeMicros; lastBindCapture = System.currentTimeMillis(); manager.notifyBindCapture(this, startNanos); @@ -68,19 +75,27 @@ public void queryPlanInit(long thresholdMicros) { /** * Collect the query plan using already captured bind values. */ - public boolean collectQueryPlan(CQueryPlanRequest request) { + public boolean collectQueryPlan(CQueryPlanRequest request, SpiTransactionManager transactionManager) { if (bindCapture == null || request.since() < lastBindCapture) { // no bind capture since the last capture return false; } + final Instant whenCaptured = Instant.ofEpochMilli(this.lastBindCapture); final BindCapture last = this.bindCapture; + final Object tenantId = this.tenantId; final long startNanos = System.nanoTime(); - SpiDbQueryPlan queryPlan = manager.collectPlan(request.connection(), this.queryPlan, last); + SpiDbQueryPlan queryPlan; + try (Connection connection = transactionManager.queryPlanConnection(tenantId)) { + queryPlan = manager.collectPlan(connection, this.queryPlan, last); + } catch (SQLException e) { + CoreLog.log.log(ERROR, "Error during query plan collection", e); + return false; + } if (queryPlan != null) { final long captureMicros = TimeUnit.MICROSECONDS.convert(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - request.add(queryPlan.with(queryTimeMicros, captureCount, captureMicros, whenCaptured)); + request.add(queryPlan.with(queryTimeMicros, captureCount, captureMicros, whenCaptured, tenantId)); // effectively turn off bind capture for this plan thresholdMicros = Long.MAX_VALUE; return true; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanManager.java index b3a4091d41..4757489288 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanManager.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.query; +import io.ebean.config.CurrentTenantProvider; import io.ebean.meta.MetaQueryPlan; import io.ebean.meta.QueryPlanRequest; import io.ebean.metric.TimedMetric; @@ -8,11 +9,9 @@ import io.ebeaninternal.server.bind.capture.BindCapture; import java.sql.Connection; -import java.sql.SQLException; import java.util.List; import java.util.concurrent.ConcurrentHashMap; -import static java.lang.System.Logger.Level.ERROR; import static java.util.Collections.emptyList; public final class CQueryPlanManager implements QueryPlanManager { @@ -21,13 +20,17 @@ public final class CQueryPlanManager implements QueryPlanManager { private final ConcurrentHashMap plans = new ConcurrentHashMap<>(); private final TransactionManager transactionManager; + private final CurrentTenantProvider tenantProvider; private final QueryPlanLogger planLogger; private final TimedMetric timeCollection; private final TimedMetric timeBindCapture; private long defaultThreshold; - public CQueryPlanManager(TransactionManager transactionManager, long defaultThreshold, QueryPlanLogger planLogger, ExtraMetrics extraMetrics) { + public CQueryPlanManager(TransactionManager transactionManager, + CurrentTenantProvider tenantProvider, + long defaultThreshold, QueryPlanLogger planLogger, ExtraMetrics extraMetrics) { this.transactionManager = transactionManager; + this.tenantProvider = tenantProvider; this.defaultThreshold = defaultThreshold; this.planLogger = planLogger; this.timeCollection = extraMetrics.planCollect(); @@ -41,7 +44,7 @@ public void setDefaultThreshold(long thresholdMicros) { @Override public SpiQueryBindCapture createBindCapture(SpiQueryPlan queryPlan) { - return new CQueryBindCapture(this, queryPlan, defaultThreshold); + return new CQueryBindCapture(this, queryPlan, defaultThreshold, tenantProvider); } public void notifyBindCapture(CQueryBindCapture planBind, long startNanos) { @@ -58,16 +61,12 @@ public List collect(QueryPlanRequest request) { } private List collectPlans(QueryPlanRequest request) { - try (Connection connection = transactionManager.queryPlanConnection()) { - CQueryPlanRequest req = new CQueryPlanRequest(connection, request, plans.keySet().iterator()); + + CQueryPlanRequest req = new CQueryPlanRequest(transactionManager, request, plans.keySet().iterator()); while (req.hasNext()) { req.nextCapture(); } return req.plans(); - } catch (SQLException e) { - CoreLog.log.log(ERROR, "Error during query plan collection", e); - return emptyList(); - } } public SpiDbQueryPlan collectPlan(Connection connection, SpiQueryPlan queryPlan, BindCapture last) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRequest.java index 573b66026c..8441f4e4b1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/CQueryPlanRequest.java @@ -2,8 +2,8 @@ import io.ebean.meta.MetaQueryPlan; import io.ebean.meta.QueryPlanRequest; +import io.ebeaninternal.api.SpiTransactionManager; -import java.sql.Connection; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -15,14 +15,15 @@ final class CQueryPlanRequest { private final List plans = new ArrayList<>(); - private final Connection connection; + private final SpiTransactionManager transactionManager; private final long since; private final int maxCount; private final long maxTime; private final Iterator iterator; - CQueryPlanRequest(Connection connection, QueryPlanRequest req, Iterator iterator) { - this.connection = connection; + + CQueryPlanRequest(SpiTransactionManager transactionManager, QueryPlanRequest req, Iterator iterator) { + this.transactionManager = transactionManager; this.iterator = iterator; this.maxCount = req.maxCount(); long reqSince = req.since(); @@ -31,13 +32,6 @@ final class CQueryPlanRequest { this.maxTime = maxTimeMillis > 0 ? System.currentTimeMillis() + maxTimeMillis : 0; } - /** - * Return the connection used to collect the db query plan. - */ - Connection connection() { - return connection; - } - /** * Add the collected query plan. */ @@ -71,7 +65,7 @@ boolean hasNext() { */ void nextCapture() { final CQueryBindCapture next = iterator.next(); - if (next.collectQueryPlan(this)) { + if (next.collectQueryPlan(this, transactionManager)) { iterator.remove(); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java index c4ffaf9ada..686e1420a9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DQueryPlanOutput.java @@ -24,6 +24,8 @@ final class DQueryPlanOutput implements MetaQueryPlan, SpiDbQueryPlan { private long captureMicros; private Instant whenCaptured; + private Object tenantId; + DQueryPlanOutput(Class beanType, String label, String hash, String sql, ProfileLocation profileLocation, String bind, String plan) { this.beanType = beanType; this.label = label; @@ -84,6 +86,14 @@ public String plan() { return plan; } + /** + * Returns the tenant id of this plan. + */ + @Override + public Object tenantId() { + return tenantId; + } + /** * Return the query execution time associated with the capture of bind values used * to build the query plan. @@ -113,18 +123,27 @@ public Instant whenCaptured() { @Override public String toString() { - return " BeanType:" + ((beanType == null) ? "" : beanType.getSimpleName()) + " planHash:" + hash + " label:" + label + " queryTimeMicros:" + queryTimeMicros + " captureCount:" + captureCount + "\n SQL:" + sql + "\nBIND:" + bind + "\nPLAN:" + plan; + return " BeanType:" + ((beanType == null) ? "" : beanType.getSimpleName()) + + " planHash:" + hash + + " label:" + label + + " queryTimeMicros:" + queryTimeMicros + + " captureCount:" + captureCount + + (tenantId == null ? "" : (" tenant:" + tenantId)) + + "\n SQL:" + sql + + "\nBIND:" + bind + + "\nPLAN:" + plan; } /** * Additionally set the query execution time and the number of bind captures. */ @Override - public DQueryPlanOutput with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured) { + public DQueryPlanOutput with(long queryTimeMicros, long captureCount, long captureMicros, Instant whenCaptured, Object tenantId) { this.queryTimeMicros = queryTimeMicros; this.captureCount = captureCount; this.captureMicros = captureMicros; this.whenCaptured = whenCaptured; + this.tenantId = tenantId; return this; } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java index c30bfcb225..821d92094e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java @@ -247,8 +247,8 @@ public final String name() { } @Override - public final Connection queryPlanConnection() throws SQLException { - return dataSourceSupplier.connection(null); + public final Connection queryPlanConnection(Object tenantId) throws SQLException { + return dataSourceSupplier.connection(tenantId); } @Override diff --git a/ebean-test/src/test/java/org/multitenant/partition/MultiTenantPartitionTest.java b/ebean-test/src/test/java/org/multitenant/partition/MultiTenantPartitionTest.java index 2f976f5b02..d8292b8d3d 100644 --- a/ebean-test/src/test/java/org/multitenant/partition/MultiTenantPartitionTest.java +++ b/ebean-test/src/test/java/org/multitenant/partition/MultiTenantPartitionTest.java @@ -5,6 +5,9 @@ import io.ebean.DatabaseBuilder; import io.ebean.config.DatabaseConfig; import io.ebean.config.TenantMode; +import io.ebean.meta.MetaQueryPlan; +import io.ebean.meta.QueryPlanInit; +import io.ebean.meta.QueryPlanRequest; import io.ebean.test.LoggedSql; import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.AfterAll; @@ -23,12 +26,13 @@ class MultiTenantPartitionTest extends BaseTestCase { static List tenants() { List tenants = new ArrayList<>(); for (int i = 0; i < 5; i++) { - tenants.add(new MtTenant("ten_"+i, names[i], names[i]+"@foo.com".toLowerCase())); + tenants.add(new MtTenant("ten_" + i, names[i], names[i] + "@foo.com".toLowerCase())); } return tenants; } private static final Database server = init(); + static { server.saveAll(tenants()); } @@ -77,6 +81,41 @@ void start() { LoggedSql.stop(); } + @Test + void queryPlanCapture() throws InterruptedException { + + QueryPlanRequest request = new QueryPlanRequest(); + request.maxCount(1_000); + request.maxTimeMillis(10_000); + server.metaInfo().queryPlanCollectNow(request); + + QueryPlanInit init = new QueryPlanInit(); + init.setAll(true); + init.thresholdMicros(1); + server.metaInfo().queryPlanInit(init); + + try { + + // run queries again + UserContext.set("rob", "ten_1"); + server.find(MtContent.class).findList(); + + UserContext.set("fred", "ten_2"); + server.find(MtContent.class).setId(2).findOne(); + + // obtains db query plans ... + List plans0 = server.metaInfo().queryPlanCollectNow(request); + assertThat(plans0).isNotEmpty(); + + assertThat(plans0).extracting(MetaQueryPlan::tenantId).containsExactlyInAnyOrder("ten_1", "ten_2"); + } finally { + // disable capturing + init.thresholdMicros(Long.MAX_VALUE); + server.metaInfo().queryPlanInit(init); + } + } + + @Test void deleteById() { UserContext.set("fred", "ten_2"); From e0084b1aa94fe07dd6f65bd202cf1ae7551f1a8d Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 14:54:07 +0200 Subject: [PATCH 06/36] Tenant partitioned caches could improve cache-hit ratio --- .../main/java/io/ebean/DatabaseBuilder.java | 11 ++ .../java/io/ebean/config/DatabaseConfig.java | 31 +++++ .../server/cache/CacheManagerOptions.java | 13 +- .../server/cache/DefaultCacheHolder.java | 50 ++++++-- .../cache/DefaultServerCacheManager.java | 4 + .../server/cache/SpiCacheManager.java | 6 + .../server/deploy/BeanDescriptor.java | 2 +- .../deploy/BeanDescriptorCacheHelp.java | 111 +++++++++--------- .../deploy/BeanDescriptorCacheHelpFixed.java | 72 ++++++++++++ .../BeanDescriptorCacheHelpPartitioned.java | 74 ++++++++++++ 10 files changed, 305 insertions(+), 69 deletions(-) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpFixed.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpPartitioned.java diff --git a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java index 985ba1760e..e0269c26b8 100644 --- a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java +++ b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java @@ -896,6 +896,12 @@ default DatabaseBuilder backgroundExecutorWrapper(BackgroundExecutorWrapper back @Deprecated DatabaseBuilder setBackgroundExecutorWrapper(BackgroundExecutorWrapper backgroundExecutorWrapper); + /** + * Sets the tenant partitioning mode for caches. This means, caches are created on demand, + * but they may not get invalidated across tenant boundaries * + */ + void setTenantPartitionedCache(boolean tenantPartitionedCache); + /** * Set the L2 cache default max size. */ @@ -2563,6 +2569,11 @@ interface Settings extends DatabaseBuilder { */ boolean isAutoPersistUpdates(); + /** + * Returns, if the caches are partitioned by tenant. + */ + boolean isTenantPartitionedCache(); + /** * Return the L2 cache default max size. */ diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java index 3c216852f8..5fbab9a1f9 100644 --- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java +++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java @@ -452,6 +452,8 @@ public class DatabaseConfig implements DatabaseBuilder.Settings { private int backgroundExecutorShutdownSecs = 30; private BackgroundExecutorWrapper backgroundExecutorWrapper = new MdcBackgroundExecutorWrapper(); + private boolean tenantPartitionedCache; + // defaults for the L2 bean caching private int cacheMaxSize = 10000; @@ -1200,6 +1202,26 @@ public DatabaseConfig setBackgroundExecutorWrapper(BackgroundExecutorWrapper bac return this; } + /** + * Returns, if the caches are partitioned by tenant. + */ + @Override + public boolean isTenantPartitionedCache() { + return tenantPartitionedCache; + } + + /** + * Sets the tenant partitioning mode for caches. This means, caches are created on demand, + * but they may not get invalidated across tenant boundaries * + */ + @Override + public void setTenantPartitionedCache(boolean tenantPartitionedCache) { + this.tenantPartitionedCache = tenantPartitionedCache; + } + + /** + * Return the L2 cache default max size. + */ @Override public int getCacheMaxSize() { return cacheMaxSize; @@ -2235,6 +2257,15 @@ protected void loadSettings(PropertiesWrapper p) { ddlPlaceholders = p.get("ddl.placeholders", ddlPlaceholders); ddlHeader = p.get("ddl.header", ddlHeader); + tenantPartitionedCache = p.getBoolean("tenantPartitionedCache", tenantPartitionedCache); + + cacheMaxIdleTime = p.getInt("cacheMaxIdleTime", cacheMaxIdleTime); + cacheMaxSize = p.getInt("cacheMaxSize", cacheMaxSize); + cacheMaxTimeToLive = p.getInt("cacheMaxTimeToLive", cacheMaxTimeToLive); + queryCacheMaxIdleTime = p.getInt("queryCacheMaxIdleTime", queryCacheMaxIdleTime); + queryCacheMaxSize = p.getInt("queryCacheMaxSize", queryCacheMaxSize); + queryCacheMaxTimeToLive = p.getInt("queryCacheMaxTimeToLive", queryCacheMaxTimeToLive); + // read tenant-configuration from config: // tenant.mode = NONE | DB | SCHEMA | CATALOG | PARTITION String mode = p.get("tenant.mode"); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java index deda681e29..b61b6bbc1c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/CacheManagerOptions.java @@ -13,18 +13,20 @@ public final class CacheManagerOptions { private final ClusterManager clusterManager; - private final DatabaseBuilder.Settings databaseBuilder; + private final String serverName; private final boolean localL2Caching; private CurrentTenantProvider currentTenantProvider; private QueryCacheEntryValidate queryCacheEntryValidate; private ServerCacheFactory cacheFactory = new DefaultServerCacheFactory(); private ServerCacheOptions beanDefault = new ServerCacheOptions(); private ServerCacheOptions queryDefault = new ServerCacheOptions(); + private final boolean tenantPartitionedCache; CacheManagerOptions() { this.localL2Caching = true; this.clusterManager = null; - this.databaseBuilder = null; + this.serverName = "db"; + this.tenantPartitionedCache = false; this.cacheFactory = new DefaultServerCacheFactory(); this.beanDefault = new ServerCacheOptions(); this.queryDefault = new ServerCacheOptions(); @@ -32,9 +34,10 @@ public final class CacheManagerOptions { public CacheManagerOptions(ClusterManager clusterManager, DatabaseBuilder.Settings config, boolean localL2Caching) { this.clusterManager = clusterManager; - this.databaseBuilder = config; + this.serverName = config.getName(); this.localL2Caching = localL2Caching; this.currentTenantProvider = config.getCurrentTenantProvider(); + this.tenantPartitionedCache = config.isTenantPartitionedCache(); } public CacheManagerOptions with(ServerCacheOptions beanDefault, ServerCacheOptions queryDefault) { @@ -55,7 +58,7 @@ public CacheManagerOptions with(CurrentTenantProvider currentTenantProvider) { } public String getServerName() { - return (databaseBuilder == null) ? "db" : databaseBuilder.getName(); + return serverName; } public boolean isLocalL2Caching() { @@ -85,4 +88,6 @@ public ClusterManager getClusterManager() { public QueryCacheEntryValidate getQueryCacheEntryValidate() { return queryCacheEntryValidate; } + + public boolean isTenantPartitionedCache() { return tenantPartitionedCache; } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java index aba5ab3638..06d322ecd1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultCacheHolder.java @@ -32,6 +32,7 @@ final class DefaultCacheHolder { private final ServerCacheOptions queryDefault; private final CurrentTenantProvider tenantProvider; private final QueryCacheEntryValidate queryCacheEntryValidate; + private final boolean tenantPartitionedCache; DefaultCacheHolder(CacheManagerOptions builder) { this.cacheFactory = builder.getCacheFactory(); @@ -39,6 +40,7 @@ final class DefaultCacheHolder { this.queryDefault = builder.getQueryDefault(); this.tenantProvider = builder.getCurrentTenantProvider(); this.queryCacheEntryValidate = builder.getQueryCacheEntryValidate(); + this.tenantPartitionedCache = builder.isTenantPartitionedCache(); } void visitMetrics(MetricVisitor visitor) { @@ -56,16 +58,42 @@ ServerCache getCache(Class beanType, String collectionProperty) { return getCacheInternal(beanType, ServerCacheType.COLLECTION_IDS, collectionProperty); } + private String key(String beanName) { + if (tenantPartitionedCache) { + StringBuilder sb = new StringBuilder(beanName.length() + 64); + sb.append(beanName); + sb.append('.'); + sb.append(tenantProvider.currentId()); + return sb.toString(); + } else { + return beanName; + } + } + private String key(String beanName, ServerCacheType type) { - return beanName + type.code(); + StringBuilder sb = new StringBuilder(beanName.length() + 64); + sb.append(beanName); + if (tenantPartitionedCache) { + sb.append('.'); + sb.append(tenantProvider.currentId()); + } + sb.append(type.code()); + return sb.toString(); } private String key(String beanName, String collectionProperty, ServerCacheType type) { + StringBuilder sb = new StringBuilder(beanName.length() + 64); + sb.append(beanName); + if (tenantPartitionedCache) { + sb.append('.'); + sb.append(tenantProvider.currentId()); + } if (collectionProperty != null) { - return beanName + "." + collectionProperty + type.code(); - } else { - return beanName + type.code(); + sb.append('.'); + sb.append(collectionProperty); } + sb.append(type.code()); + return sb.toString(); } /** @@ -82,12 +110,17 @@ private ServerCache createCache(Class beanType, ServerCacheType type, String if (type == ServerCacheType.COLLECTION_IDS) { lock.lock(); try { - collectIdCaches.computeIfAbsent(beanType.getName(), s -> new ConcurrentSkipListSet<>()).add(key); + collectIdCaches.computeIfAbsent(key(beanType.getName()), s -> new ConcurrentSkipListSet<>()).add(key); } finally { lock.unlock(); } } - return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate)); + if (tenantPartitionedCache) { + return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, null, queryCacheEntryValidate)); + } else { + return cacheFactory.createCache(new ServerCacheConfig(type, key, shortName, options, tenantProvider, queryCacheEntryValidate)); + } + } void clearAll() { @@ -103,7 +136,7 @@ public void clear(String name) { clearIfExists(key(name, ServerCacheType.QUERY)); clearIfExists(key(name, ServerCacheType.BEAN)); clearIfExists(key(name, ServerCacheType.NATURAL_KEY)); - Set keys = collectIdCaches.get(name); + Set keys = collectIdCaches.get(key(name)); if (keys != null) { for (String collectionIdKey : keys) { clearIfExists(collectionIdKey); @@ -147,4 +180,7 @@ private ServerCacheOptions getBeanOptions(Class cls) { return beanDefault.copy(nearCache); } + boolean isTenantPartitionedCache() { + return tenantPartitionedCache; + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java index 0b7264ff90..39558d19d8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCacheManager.java @@ -154,4 +154,8 @@ public ServerCache getBeanCache(Class beanType) { return cacheHolder.getCache(beanType, ServerCacheType.BEAN); } + @Override + public boolean isTenantPartitionedCache() { + return cacheHolder.isTenantPartitionedCache(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java index bf6d4d5a21..ebeec00f68 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/SpiCacheManager.java @@ -88,4 +88,10 @@ public interface SpiCacheManager { */ void clearLocal(Class beanType); + /** + * returns true, if this chacheManager runs in tenant partitioned mode + * @return + */ + boolean isTenantPartitionedCache(); + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index b0988c2e35..dcebbce4be 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -318,7 +318,7 @@ public BeanDescriptor(BeanDescriptorMap owner, DeployBeanDescriptor deploy) { this.idOnlyReference = isIdOnlyReference(propertiesBaseScalar); boolean noRelationships = propertiesOne.length + propertiesMany.length == 0; this.cacheSharableBeans = noRelationships && deploy.getCacheOptions().isReadOnly(); - this.cacheHelp = new BeanDescriptorCacheHelp<>(this, owner.cacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported); + this.cacheHelp = BeanDescriptorCacheHelpPartitioned.create(this, owner.cacheManager(), deploy.getCacheOptions(), cacheSharableBeans, propertiesOneImported); this.jsonHelp = initJsonHelp(); this.draftHelp = new BeanDescriptorDraftHelp<>(this); this.docStoreAdapter = owner.createDocStoreBeanAdapter(this, deploy); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java index 50aca35044..ee71f7af33 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java @@ -25,7 +25,7 @@ * * @param The entity bean type */ -final class BeanDescriptorCacheHelp { +abstract class BeanDescriptorCacheHelp { private static final System.Logger log = CoreLog.internal; @@ -34,7 +34,7 @@ final class BeanDescriptorCacheHelp { private static final System.Logger manyLog = AppLog.getLogger("io.ebean.cache.COLL"); private static final System.Logger natLog = AppLog.getLogger("io.ebean.cache.NATKEY"); - private final BeanDescriptor desc; + final BeanDescriptor desc; private final SpiCacheManager cacheManager; private final CacheOptions cacheOptions; /** @@ -42,13 +42,10 @@ final class BeanDescriptorCacheHelp { */ private final boolean cacheSharableBeans; private final boolean invalidateQueryCache; - private final Class beanType; + final Class beanType; private final String cacheName; private final BeanPropertyAssocOne[] propertiesOneImported; private final String[] naturalKey; - private final ServerCache beanCache; - private final ServerCache naturalKeyCache; - private final ServerCache queryCache; private final boolean noCaching; private final SpiCacheControl cacheControl; private final SpiCacheRegion cacheRegion; @@ -61,6 +58,15 @@ final class BeanDescriptorCacheHelp { */ private boolean cacheNotifyOnDelete; + static BeanDescriptorCacheHelp create(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, + boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { + if ((cacheOptions.isEnableQueryCache() || cacheOptions.isEnableBeanCache()) && cacheManager.isTenantPartitionedCache()) { + return new BeanDescriptorCacheHelpPartitioned<>(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + } else { + return new BeanDescriptorCacheHelpFixed<>(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + } + } + BeanDescriptorCacheHelp(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { this.desc = desc; @@ -72,38 +78,38 @@ final class BeanDescriptorCacheHelp { this.cacheSharableBeans = cacheSharableBeans; this.propertiesOneImported = propertiesOneImported; this.naturalKey = cacheOptions.getNaturalKey(); - if (!cacheOptions.isEnableQueryCache()) { - this.queryCache = null; - } else { - this.queryCache = cacheManager.getQueryCache(beanType); - } - if (cacheOptions.isEnableBeanCache()) { - this.beanCache = cacheManager.getBeanCache(beanType); - if (cacheOptions.getNaturalKey() != null) { - this.naturalKeyCache = cacheManager.getNaturalKeyCache(beanType); - } else { - this.naturalKeyCache = null; - } - } else { - this.beanCache = null; - this.naturalKeyCache = null; - } - this.noCaching = (beanCache == null && queryCache == null); + this.noCaching = !cacheOptions.isEnableQueryCache() && !cacheOptions.isEnableBeanCache(); if (noCaching) { this.cacheControl = DCacheControlNone.INSTANCE; this.cacheRegion = (invalidateQueryCache) ? cacheManager.getRegion(cacheOptions.getRegion()) : DCacheRegionNone.INSTANCE; } else { this.cacheRegion = cacheManager.getRegion(cacheOptions.getRegion()); - this.cacheControl = new DCacheControl(cacheRegion, (beanCache != null), (naturalKeyCache != null), (queryCache != null)); + this.cacheControl = new DCacheControl(cacheRegion, + cacheOptions.isEnableBeanCache(), + cacheOptions.isEnableBeanCache() && cacheOptions.getNaturalKey() != null, + cacheOptions.isEnableQueryCache()); } } + abstract boolean hasBeanCache(); + + abstract boolean hasQueryCache(); + + abstract ServerCache getQueryCache(); + + abstract ServerCache getNaturalKeyCache(); + + abstract ServerCache getBeanCache(); + + /** * Derive the cache notify flags. */ void deriveNotifyFlags() { - cacheNotifyOnAll = (invalidateQueryCache || beanCache != null || queryCache != null); + cacheNotifyOnAll = (invalidateQueryCache + || hasBeanCache() + || hasQueryCache()); cacheNotifyOnDelete = !cacheNotifyOnAll && isNotifyOnDeletes(); if (log.isLoggable(DEBUG)) { if (cacheNotifyOnAll || cacheNotifyOnDelete) { @@ -178,11 +184,11 @@ CacheOptions getCacheOptions() { * Clear the query cache. */ void queryCacheClear() { - if (queryCache != null) { + if (hasQueryCache()) { if (queryLog.isLoggable(DEBUG)) { queryLog.log(DEBUG, " CLEAR {0}", cacheName); } - queryCache.clear(); + getQueryCache().clear(); } } @@ -190,7 +196,7 @@ void queryCacheClear() { * Add query cache clear to the changeSet. */ private void queryCacheClear(CacheChangeSet changeSet) { - if (queryCache != null) { + if (hasQueryCache()) { changeSet.addClearQuery(desc); } } @@ -199,10 +205,10 @@ private void queryCacheClear(CacheChangeSet changeSet) { * Get a query result from the query cache. */ Object queryCacheGet(Object id) { - if (queryCache == null) { + if (!hasQueryCache()) { throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)"); } - Object queryResult = queryCache.get(id); + Object queryResult = getQueryCache().get(id); if (queryLog.isLoggable(DEBUG)) { if (queryResult == null) { queryLog.log(DEBUG, " GET {0}({1}) - cache miss", cacheName, id); @@ -217,13 +223,13 @@ Object queryCacheGet(Object id) { * Put a query result into the query cache. */ void queryCachePut(Object id, QueryCacheEntry entry) { - if (queryCache == null) { + if (!hasQueryCache()) { throw new IllegalStateException("No query cache enabled on " + desc + ". Need explicit @Cache(enableQueryCache=true)"); } if (queryLog.isLoggable(DEBUG)) { queryLog.log(DEBUG, " PUT {0}({1})", cacheName, id); } - queryCache.put(id, entry); + getQueryCache().put(id, entry); } void manyPropRemove(String propertyName, String parentKey) { @@ -294,7 +300,7 @@ boolean manyPropLoad(BeanPropertyAssocMany many, BeanCollection bc, String */ void manyPropPut(BeanPropertyAssocMany many, Object details, String parentKey) { if (many.isElementCollection()) { - CachedBeanData data = (CachedBeanData) beanCache.get(parentKey); + CachedBeanData data = (CachedBeanData) getBeanCache().get(parentKey); if (data != null) { try { // add as JSON to bean cache @@ -306,7 +312,7 @@ void manyPropPut(BeanPropertyAssocMany many, Object details, String parentKey if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " UPDATE {0}({1}) changes:{2}", cacheName, parentKey, changes); } - beanCache.put(parentKey, newData); + getBeanCache().put(parentKey, newData); } catch (IOException e) { log.log(ERROR, "Error updating L2 cache", e); } @@ -352,7 +358,7 @@ BeanCacheResult cacheIdLookup(PersistenceContext context, Collection ids) if (ids.isEmpty()) { return new BeanCacheResult<>(); } - Map beanDataMap = beanCache.getAll(keys); + Map beanDataMap = getBeanCache().getAll(keys); if (beanLog.isLoggable(TRACE)) { beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet()); } @@ -374,7 +380,7 @@ BeanCacheResult naturalKeyLookup(PersistenceContext context, Set keys } // naturalKey -> Id map - Map naturalKeyMap = naturalKeyCache.getAll(keys); + Map naturalKeyMap = getNaturalKeyCache().getAll(keys); if (natLog.isLoggable(TRACE)) { natLog.log(TRACE, " MLOOKUP {0}({1}) - hits:{2}", cacheName, keys, naturalKeyMap); } @@ -391,7 +397,7 @@ BeanCacheResult naturalKeyLookup(PersistenceContext context, Set keys } Set ids = new HashSet<>(naturalKeyMap.values()); - Map beanDataMap = beanCache.getAll(ids); + Map beanDataMap = getBeanCache().getAll(ids); if (beanLog.isLoggable(TRACE)) { beanLog.log(TRACE, " MGET {0}({1}) - hits:{2}", cacheName, ids, beanDataMap.keySet()); } @@ -422,25 +428,16 @@ private void setupContext(Object bean, PersistenceContext context) { desc.contextPut(context, id, bean); } - /** - * Return the beanCache creating it if necessary. - */ - private ServerCache getBeanCache() { - if (beanCache == null) { - throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation."); - } - return beanCache; - } /** * Clear the bean cache. */ void beanCacheClear() { - if (beanCache != null) { + if (hasBeanCache()) { if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " CLEAR {0}", cacheName); } - beanCache.clear(); + getBeanCache().clear(); } } @@ -516,7 +513,7 @@ void beanCachePutAllDirect(Collection beans) { if (natLog.isLoggable(DEBUG)) { natLog.log(DEBUG, " MPUT {0}({1}, {2})", cacheName, Arrays.toString(naturalKey), natKeys.keySet()); } - naturalKeyCache.putAll(natKeys); + getNaturalKeyCache().putAll(natKeys); } } @@ -536,7 +533,7 @@ void beanCachePutDirect(EntityBean bean) { if (natLog.isLoggable(DEBUG)) { natLog.log(DEBUG, " PUT {0}({1}, {2})", cacheName, naturalKey, key); } - naturalKeyCache.put(naturalKey, key); + getNaturalKeyCache().put(naturalKey, key); } } } @@ -679,11 +676,11 @@ EntityBean embeddedBeanLoadDirect(CachedBeanData data, PersistenceContext contex * Remove a bean from the cache given its Id. */ void beanCacheApplyInvalidate(Collection keys) { - if (beanCache != null) { + if (hasBeanCache()) { if (beanLog.isLoggable(DEBUG)) { beanLog.log(DEBUG, " MREMOVE {0}({1})", cacheName, keys); } - beanCache.removeAll(new HashSet<>(keys)); + getBeanCache().removeAll(new HashSet<>(keys)); } for (BeanPropertyAssocOne imported : propertiesOneImported) { imported.cacheClear(); @@ -767,7 +764,7 @@ void persistDeleteIds(Collection ids, CacheChangeSet changeSet) { changeSet.addInvalidate(desc); } else { queryCacheClear(changeSet); - if (beanCache != null) { + if (hasBeanCache()) { changeSet.addBeanRemoveMany(desc, ids); } cacheDeleteImported(true, null, changeSet); @@ -782,7 +779,7 @@ void persistDelete(Object id, PersistRequestBean deleteRequest, CacheChangeSe changeSet.addInvalidate(desc); } else { queryCacheClear(changeSet); - if (beanCache != null) { + if (hasBeanCache()) { changeSet.addBeanRemove(desc, id); } cacheDeleteImported(true, deleteRequest.entityBean(), changeSet); @@ -817,7 +814,7 @@ void persistUpdate(Object id, PersistRequestBean updateRequest, CacheChangeSe } else { queryCacheClear(changeSet); - if (beanCache == null) { + if (!hasBeanCache()) { // query caching only return; } @@ -860,7 +857,7 @@ void persistTableIUD(TableIUD tableIUD, CacheChangeSet changeSet) { void cacheNaturalKeyPut(String key, String newKey) { if (newKey != null) { - naturalKeyCache.put(newKey, key); + getNaturalKeyCache().put(newKey, key); } } @@ -893,7 +890,7 @@ void cacheBeanUpdate(String key, Map changes, boolean updateNatu if (natLog.isLoggable(DEBUG)) { natLog.log(DEBUG, ".. update {0} REMOVE({1}) - old key for ({2})", cacheName, oldKey, key); } - naturalKeyCache.remove(oldKey); + getNaturalKeyCache().remove(oldKey); } } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpFixed.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpFixed.java new file mode 100644 index 0000000000..448f661cce --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpFixed.java @@ -0,0 +1,72 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.cache.ServerCache; +import io.ebeaninternal.server.cache.SpiCacheManager; +import io.ebeaninternal.server.core.CacheOptions; + + +/** + * Helper for BeanDescriptor that manages the bean, query and collection caches. + * + * @param The entity bean type + */ +final class BeanDescriptorCacheHelpFixed extends BeanDescriptorCacheHelp { + private final ServerCache beanCache; + private final ServerCache naturalKeyCache; + private final ServerCache queryCache; + + + BeanDescriptorCacheHelpFixed(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, + boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { + super(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + if (!cacheOptions.isEnableQueryCache()) { + this.queryCache = null; + } else { + this.queryCache = cacheManager.getQueryCache(beanType); + } + + if (cacheOptions.isEnableBeanCache()) { + this.beanCache = cacheManager.getBeanCache(beanType); + if (cacheOptions.getNaturalKey() != null) { + this.naturalKeyCache = cacheManager.getNaturalKeyCache(beanType); + } else { + this.naturalKeyCache = null; + } + } else { + this.beanCache = null; + this.naturalKeyCache = null; + } + } + + @Override + boolean hasBeanCache() { + return beanCache != null; + } + + @Override + boolean hasQueryCache() { + return queryCache != null; + } + + + @Override + ServerCache getQueryCache() { + return queryCache; + } + + @Override + ServerCache getNaturalKeyCache() { + return naturalKeyCache; + } + + /** + * Return the beanCache creating it if necessary. + */ + @Override + ServerCache getBeanCache() { + if (beanCache == null) { + throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation."); + } + return beanCache; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpPartitioned.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpPartitioned.java new file mode 100644 index 0000000000..0e6ccd537f --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorCacheHelpPartitioned.java @@ -0,0 +1,74 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.cache.ServerCache; +import io.ebeaninternal.server.cache.SpiCacheManager; +import io.ebeaninternal.server.core.CacheOptions; + +import java.util.function.Supplier; + +/** + * Helper for BeanDescriptor that manages the bean, query and collection caches. + * + * @param The entity bean type + */ +final class BeanDescriptorCacheHelpPartitioned extends BeanDescriptorCacheHelp { + private final Supplier beanCacheSupplier; + private final Supplier naturalKeyCacheSupplier; + private final Supplier queryCacheSupplier; + + + BeanDescriptorCacheHelpPartitioned(BeanDescriptor desc, SpiCacheManager cacheManager, CacheOptions cacheOptions, + boolean cacheSharableBeans, BeanPropertyAssocOne[] propertiesOneImported) { + super(desc, cacheManager, cacheOptions, cacheSharableBeans, propertiesOneImported); + if (!cacheOptions.isEnableQueryCache()) { + this.queryCacheSupplier = null; + } else { + this.queryCacheSupplier = () -> cacheManager.getQueryCache(beanType); + } + + if (cacheOptions.isEnableBeanCache()) { + this.beanCacheSupplier = () -> cacheManager.getBeanCache(beanType); + if (cacheOptions.getNaturalKey() != null) { + this.naturalKeyCacheSupplier = () -> cacheManager.getNaturalKeyCache(beanType); + } else { + this.naturalKeyCacheSupplier = null; + } + } else { + this.beanCacheSupplier = null; + this.naturalKeyCacheSupplier = null; + } + } + + @Override + boolean hasBeanCache() { + return beanCacheSupplier != null; + } + + @Override + boolean hasQueryCache() { + return queryCacheSupplier != null; + } + + + @Override + ServerCache getQueryCache() { + return queryCacheSupplier.get(); + } + + @Override + ServerCache getNaturalKeyCache() { + return naturalKeyCacheSupplier.get(); + } + + /** + * Return the beanCache creating it if necessary. + */ + @Override + ServerCache getBeanCache() { + if (beanCacheSupplier != null) { + return beanCacheSupplier.get(); + } else { + throw new IllegalStateException("No bean cache enabled for " + desc + ". Add the @Cache annotation."); + } + } +} From 146e11fa64cb511f38e9bb59b55761456fae7287 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 14:56:16 +0200 Subject: [PATCH 07/36] checkUniqueness supports queryCache and skipClean checkuniqueness --- ebean-api/src/main/java/io/ebean/DB.java | 4 +- .../src/main/java/io/ebean/Database.java | 13 +- .../server/core/DefaultServer.java | 37 ++++-- .../xtest/internal/api/TDSpiEbeanServer.java | 7 +- .../ebean/xtest/internal/api/TDSpiServer.java | 7 +- .../tests/insert/TestInsertCheckUnique.java | 119 +++++++++++++++++- .../model/basic/EBasicWithUniqueCon.java | 2 + 7 files changed, 162 insertions(+), 27 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/DB.java b/ebean-api/src/main/java/io/ebean/DB.java index d8b68e7ad4..faa6266b6c 100644 --- a/ebean-api/src/main/java/io/ebean/DB.java +++ b/ebean-api/src/main/java/io/ebean/DB.java @@ -458,8 +458,8 @@ public static Set checkUniqueness(Object bean) { /** * Same as {@link #checkUniqueness(Object)} but with given transaction. */ - public static Set checkUniqueness(Object bean, Transaction transaction) { - return getDefault().checkUniqueness(bean, transaction); + public static Set checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) { + return getDefault().checkUniqueness(bean, transaction, useQueryCache, skipClean); } /** diff --git a/ebean-api/src/main/java/io/ebean/Database.java b/ebean-api/src/main/java/io/ebean/Database.java index 8fee6929a3..202c347e67 100644 --- a/ebean-api/src/main/java/io/ebean/Database.java +++ b/ebean-api/src/main/java/io/ebean/Database.java @@ -1085,12 +1085,21 @@ static DatabaseBuilder builder() { * @param bean The entity bean to check uniqueness on * @return a set of Properties if constraint validation was detected or empty list. */ - Set checkUniqueness(Object bean); + default Set checkUniqueness(Object bean) { + return checkUniqueness(bean, null, false, true); + } /** * Same as {@link #checkUniqueness(Object)}. but with given transaction. */ - Set checkUniqueness(Object bean, Transaction transaction); + default Set checkUniqueness(Object bean, Transaction transaction) { + return checkUniqueness(bean, transaction, false, true); + } + + /** + * Same as {@link #checkUniqueness(Object)}. but with given transaction and extended search options. + */ + Set checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean); /** * Marks the entity bean as dirty. 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 26af43658d..b876b0f916 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 @@ -2211,12 +2211,7 @@ public void slowQueryCheck(long timeMicros, int rowCount, SpiQuery query) { } @Override - public Set checkUniqueness(Object bean) { - return checkUniqueness(bean, null); - } - - @Override - public Set checkUniqueness(Object bean, @Nullable Transaction transaction) { + public Set checkUniqueness(Object bean, @Nullable Transaction transaction, boolean useQueryCache, boolean skipClean) { EntityBean entityBean = checkEntityBean(bean); BeanDescriptor beanDesc = descriptor(entityBean.getClass()); BeanProperty idProperty = beanDesc.idProperty(); @@ -2228,14 +2223,15 @@ public Set checkUniqueness(Object bean, @Nullable Transaction transact if (entityBean._ebean_getIntercept().isNew() && id != null) { // Primary Key is changeable only on new models - so skip check if we are not new SpiQuery query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory); + query.setUseQueryCache(useQueryCache); query.usingTransaction(transaction); query.setId(id); - if (findCount(query) > 0) { + if (exists(query)) { return Collections.singleton(idProperty); } } for (BeanProperty[] props : beanDesc.uniqueProps()) { - Set ret = checkUniqueness(entityBean, beanDesc, props, transaction); + Set ret = checkUniqueness(entityBean, beanDesc, props, transaction, useQueryCache, skipClean); if (ret != null) { return ret; } @@ -2243,13 +2239,34 @@ public Set checkUniqueness(Object bean, @Nullable Transaction transact return Collections.emptySet(); } + /** + * Checks, if any property is dirty. + */ + private boolean isAnyPropertyDirty(EntityBean entityBean, BeanProperty[] props) { + if (entityBean._ebean_getIntercept().isNew()) { + return true; + } + for (BeanProperty prop : props) { + if (entityBean._ebean_getIntercept().isDirtyProperty(prop.propertyIndex())) { + return true; + } + } + return false; + } + /** * Returns a set of properties if saving the bean will violate the unique constraints (defined by given properties). */ @Nullable - private Set checkUniqueness(EntityBean entityBean, BeanDescriptor beanDesc, BeanProperty[] props, @Nullable Transaction transaction) { + private Set checkUniqueness(EntityBean entityBean, BeanDescriptor beanDesc, BeanProperty[] props, @Nullable Transaction transaction, + boolean useQueryCache, boolean skipClean) { + if (skipClean && !isAnyPropertyDirty(entityBean, props)) { + return null; + } + BeanProperty idProperty = beanDesc.idProperty(); SpiQuery query = new DefaultOrmQuery<>(beanDesc, this, expressionFactory); + query.setUseQueryCache(useQueryCache); query.usingTransaction(transaction); ExpressionList exprList = query.where(); if (!entityBean._ebean_getIntercept().isNew()) { @@ -2263,7 +2280,7 @@ private Set checkUniqueness(EntityBean entityBean, BeanDescriptor b } exprList.eq(prop.name(), value); } - if (findCount(query) > 0) { + if (exists(query)) { Set ret = new LinkedHashSet<>(); Collections.addAll(ret, props); return ret; diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java index 0b7a757bca..71160f1525 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java @@ -950,12 +950,7 @@ public void slowQueryCheck(long executionTimeMicros, int rowCount, SpiQuery q } @Override - public Set checkUniqueness(Object bean) { - return Collections.emptySet(); - } - - @Override - public Set checkUniqueness(Object bean, Transaction transaction) { + public Set checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) { return Collections.emptySet(); } diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java index a1fcd2777f..20d8856150 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java @@ -384,12 +384,7 @@ public int saveAll(Collection beans, Transaction transaction) throws Optimist } @Override - public Set checkUniqueness(Object bean) { - return null; - } - - @Override - public Set checkUniqueness(Object bean, Transaction transaction) { + public Set checkUniqueness(Object bean, Transaction transaction, boolean useQueryCache, boolean skipClean) { return null; } diff --git a/ebean-test/src/test/java/org/tests/insert/TestInsertCheckUnique.java b/ebean-test/src/test/java/org/tests/insert/TestInsertCheckUnique.java index c7cf31b77c..d60d2c47e2 100644 --- a/ebean-test/src/test/java/org/tests/insert/TestInsertCheckUnique.java +++ b/ebean-test/src/test/java/org/tests/insert/TestInsertCheckUnique.java @@ -1,13 +1,16 @@ package org.tests.insert; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Transaction; import io.ebean.plugin.Property; +import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.tests.model.basic.EBasicWithUniqueCon; import org.tests.model.draftable.Document; +import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -102,9 +105,123 @@ public void example() { System.out.println("uniqueProperties > " + uniqueProperties); System.out.println(" custom msg > " + msg); + } + LoggedSql.start(); assertThat(DB.checkUniqueness(doc2).toString()).contains("title"); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select").contains(" t0.id from document t0 where t0.title = ?"); + + + } + } + + /** + * When invoking checkUniqueness multiple times, we can benefit from the "exists" query cache if bean has query cache enabled + */ + @Test + public void testUseQueryCache() { + DB.find(EBasicWithUniqueCon.class).delete(); // clean up DB (otherwise test may be affected by other test) + + EBasicWithUniqueCon basic = new EBasicWithUniqueCon(); + basic.setName("foo"); + basic.setOther("bar"); + basic.setOtherOne("baz"); + + // create a new bean + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, null, true, false)).isEmpty(); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select").contains("t0.id from e_basicverucon t0 where t0.name = ?"); + assertThat(sql.get(1)).contains("select").contains("t0.id from e_basicverucon t0 where t0.other = ? and t0.other_one = ?"); + DB.save(basic); + try { + // reload from database + basic = DB.find(EBasicWithUniqueCon.class, basic.getId()); + + // and check again + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, null, true, false)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select").contains("t0.id from e_basicverucon t0 where t0.id <> ? and t0.name = ?"); + assertThat(sql.get(1)).contains("select").contains("t0.id from e_basicverucon t0 where t0.id <> ? and t0.other = ? and t0.other_one = ?"); + + // and check again - expect to hit query cache + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, null, true, false)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).as("Expected to hit query cache").hasSize(0); + + // and check again, where only one value is changed + basic.setOther("fooo"); + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, null, true, false)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("fooo,baz)"); + + } finally { + DB.delete(EBasicWithUniqueCon.class, basic.getId()); + } + } + + + /** + * When invoking checkUniqueness multiple times, we can benefit from the "exists" query cache if bean has query cache enabled + */ + @Test + public void testSkipClean() { + DB.find(EBasicWithUniqueCon.class).delete(); // clean up DB (otherwise test may be affected by other test) + + EBasicWithUniqueCon basic = new EBasicWithUniqueCon(); + basic.setName("foo"); + basic.setOther("bar"); + basic.setOtherOne("baz"); + + // create a new bean + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, null, false, true)).isEmpty(); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); + assertThat(sql.get(0)).contains("select").contains("t0.id from e_basicverucon t0 where t0.name = ?"); + assertThat(sql.get(1)).contains("select").contains("t0.id from e_basicverucon t0 where t0.other = ? and t0.other_one = ?"); + DB.save(basic); + try (Transaction txn = DB.beginTransaction()) { + // reload from database + basic = DB.find(EBasicWithUniqueCon.class, basic.getId()); + + // and check again. We do not check unmodified properties + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, txn, false, true)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(0); + + // and check again, where only one value is changed + basic.setOther("fooo"); + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, txn, false, true)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("fooo,baz)"); + + // multiple checks will hit DB + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, txn, false, true)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(1); + + // enable also query cache + assertThat(DB.checkUniqueness(basic, txn, true, true)).isEmpty(); + LoggedSql.start(); + assertThat(DB.checkUniqueness(basic, txn, true, true)).isEmpty(); + sql = LoggedSql.stop(); + assertThat(sql).isEmpty(); + } finally { + DB.delete(EBasicWithUniqueCon.class, basic.getId()); } } } diff --git a/ebean-test/src/test/java/org/tests/model/basic/EBasicWithUniqueCon.java b/ebean-test/src/test/java/org/tests/model/basic/EBasicWithUniqueCon.java index 7e416bd531..87c63d3bfa 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/EBasicWithUniqueCon.java +++ b/ebean-test/src/test/java/org/tests/model/basic/EBasicWithUniqueCon.java @@ -1,10 +1,12 @@ package org.tests.model.basic; +import io.ebean.annotation.Cache; import jakarta.persistence.*; import javax.validation.constraints.Size; import java.sql.Timestamp; @Entity +@Cache(enableQueryCache = true) @Table(name = "e_basicverucon") @UniqueConstraint(columnNames = {"other", "other_one"}) public class EBasicWithUniqueCon { From f9680fc9d415d9cd3bbf0d96028f0a7a349ab9d1 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 15:09:02 +0200 Subject: [PATCH 08/36] DbJson Support for Dto-Queries --- .../deploy/meta/DeployBeanProperty.java | 14 +++- .../server/deploy/meta/DeployProperty.java | 53 ++++++++++++ .../server/deploy/parse/DeployUtil.java | 2 +- .../server/dto/DtoMetaBuilder.java | 10 ++- .../server/dto/DtoMetaDeployProperty.java | 81 +++++++++++++++++++ .../server/dto/DtoMetaProperty.java | 71 +++++++++++++++- .../server/type/DefaultTypeManager.java | 27 +++++-- .../server/type/TypeManager.java | 12 ++- .../org/tests/json/TestDbJson_Jackson.java | 75 ++++++++++++++++- 9 files changed, 325 insertions(+), 20 deletions(-) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployProperty.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaDeployProperty.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index 6527f51950..fa96d959ea 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -38,7 +38,7 @@ * Description of a property of a bean. Includes its deployment information such * as database column mapping information. */ -public class DeployBeanProperty { +public class DeployBeanProperty implements DeployProperty { private static final int ID_ORDER = 1000000; private static final int UNIDIRECTIONAL_ORDER = 100000; @@ -229,6 +229,11 @@ public DeployBeanDescriptor getDesc() { return desc; } + @Override + public Class getOwnerType() { + return desc.getBeanType(); + } + /** * Return the DB column length for character columns. *

@@ -261,10 +266,12 @@ public void setJsonDeserialize(boolean jsonDeserialize) { this.jsonDeserialize = jsonDeserialize; } + @Override public MutationDetection getMutationDetection() { return mutationDetection; } + @Override public void setMutationDetection(MutationDetection dirtyDetection) { this.mutationDetection = dirtyDetection; } @@ -479,8 +486,9 @@ public void setGeneratedProperty(GeneratedProperty generatedValue) { } /** - * Return true if this property is mandatory. + * Return true if this property is not mandatory. */ + @Override public boolean isNullable() { return nullable; } @@ -851,6 +859,7 @@ public Class getPropertyType() { /** * Return the generic type for this property. */ + @Override public Type getGenericType() { return genericType; } @@ -1055,6 +1064,7 @@ public A getMetaAnnotation(Class annotationType) { return null; } + @Override @SuppressWarnings("unchecked") public List getMetaAnnotations(Class annotationType) { List result = new ArrayList<>(); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployProperty.java new file mode 100644 index 0000000000..0fe3a4b8f1 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployProperty.java @@ -0,0 +1,53 @@ +package io.ebeaninternal.server.deploy.meta; + +import io.ebean.annotation.MutationDetection; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Property, with basic type information (BeanProperty and DtoProperty). + */ +public interface DeployProperty { + + /** + * Return the name of the property. + */ + String getName(); + + /** + * Return the generic type for this property. + */ + Type getGenericType(); + + /** + * Return the property type. + */ + Class getPropertyType(); + + /** + * Returns the owner class of this property. + */ + Class getOwnerType(); + + /** + * Returns the annotations on this property. + */ + List getMetaAnnotations(Class annotationType); + + /** + * Returns the mutation detection setting of this property. + */ + MutationDetection getMutationDetection(); + + /** + * Sets the mutation detection setting of this property. + */ + void setMutationDetection(MutationDetection mutationDetection); + + /** + * Return true if this property is not mandatory. + */ + boolean isNullable(); +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java index 8da22e0c48..c5005bed2a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployUtil.java @@ -219,7 +219,7 @@ private void setDbJsonType(DeployBeanProperty prop, int dbType, int dbLength, Mu /** * Return the JDBC type for the JSON storage type. */ - private int dbJsonStorage(DbJsonType dbJsonType) { + public static int dbJsonStorage(DbJsonType dbJsonType) { switch (dbJsonType) { case JSONB: return DbPlatformType.JSONB; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java index 06929cefeb..bb6dde7fa3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaBuilder.java @@ -1,5 +1,7 @@ package io.ebeaninternal.server.dto; +import io.ebean.annotation.DbJson; +import io.ebean.annotation.DbJsonB; import io.ebeaninternal.api.CoreLog; import io.ebeaninternal.server.type.TypeManager; @@ -21,10 +23,16 @@ final class DtoMetaBuilder { private final Class dtoType; private final List properties = new ArrayList<>(); private final Map constructorMap = new HashMap<>(); + private final Set> annotationFilter = new HashSet<>(); DtoMetaBuilder(Class dtoType, TypeManager typeManager) { this.dtoType = dtoType; this.typeManager = typeManager; + annotationFilter.add(DbJson.class); + annotationFilter.add(DbJsonB.class); + if (typeManager.jsonMarkerAnnotation() != null) { + annotationFilter.add(typeManager.jsonMarkerAnnotation()); + } } DtoMeta build() { @@ -38,7 +46,7 @@ private void readProperties() { if (includeMethod(method)) { try { final String name = propertyName(method.getName()); - properties.add(new DtoMetaProperty(typeManager, dtoType, method, name)); + properties.add(new DtoMetaProperty(typeManager, dtoType, method, name, annotationFilter)); } catch (Exception e) { CoreLog.log.log(DEBUG, "exclude on " + dtoType + " method " + method, e); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaDeployProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaDeployProperty.java new file mode 100644 index 0000000000..d4519fe509 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaDeployProperty.java @@ -0,0 +1,81 @@ +package io.ebeaninternal.server.dto; + +import io.ebean.annotation.MutationDetection; +import io.ebeaninternal.server.deploy.meta.DeployProperty; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * DeployProperty for Dto-Properties. + * + * @author Roland Praml, FOCONIS AG + */ +class DtoMetaDeployProperty implements DeployProperty { + private final String name; + private final Class ownerType; + private final Type genericType; + private final Class propertyType; + private final Set metaAnnotations; + private final boolean nullable; + private MutationDetection mutationDetection = MutationDetection.DEFAULT; + + DtoMetaDeployProperty(String name, Class ownerType, Type genericType, Class propertyType, Set metaAnnotations, Method method) { + this.name = name; + this.ownerType = ownerType; + this.genericType = genericType; + this.nullable = !propertyType.isPrimitive(); + this.propertyType = propertyType; + this.metaAnnotations = metaAnnotations; + } + + @Override + public String getName() { + return name; + } + + @Override + public Type getGenericType() { + return genericType; + } + + @Override + public Class getPropertyType() { + return propertyType; + } + + @Override + public Class getOwnerType() { + return ownerType; + } + + @Override + public List getMetaAnnotations(Class annotationType) { + List result = new ArrayList<>(); + for (Annotation ann : metaAnnotations) { + if (ann.annotationType() == annotationType) { + result.add((A) ann); + } + } + return result; + } + + @Override + public MutationDetection getMutationDetection() { + return mutationDetection; + } + + @Override + public void setMutationDetection(MutationDetection mutationDetection) { + this.mutationDetection = mutationDetection; + } + + @Override + public boolean isNullable() { + return nullable; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java index 680c0095b6..6a052fe05c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/dto/DtoMetaProperty.java @@ -1,15 +1,26 @@ package io.ebeaninternal.server.dto; +import io.ebean.annotation.DbJson; +import io.ebean.annotation.DbJsonB; +import io.ebean.config.dbplatform.DbPlatformType; import io.ebean.core.type.DataReader; import io.ebean.core.type.ScalarType; +import io.ebean.util.AnnotationUtil; +import io.ebeaninternal.server.deploy.meta.DeployProperty; +import io.ebeaninternal.server.deploy.parse.DeployUtil; import io.ebeaninternal.server.type.TypeManager; +import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.sql.SQLException; +import java.util.Collections; +import java.util.List; +import java.util.Set; final class DtoMetaProperty implements DtoReadSet { @@ -20,18 +31,74 @@ final class DtoMetaProperty implements DtoReadSet { private final MethodHandle setter; private final ScalarType scalarType; - DtoMetaProperty(TypeManager typeManager, Class dtoType, Method writeMethod, String name) throws IllegalAccessException, NoSuchMethodException { + DtoMetaProperty(TypeManager typeManager, Class dtoType, Method writeMethod, String name, Set> annotationFilter) + throws IllegalAccessException, NoSuchMethodException { this.dtoType = dtoType; this.name = name; if (writeMethod != null) { this.setter = lookupMethodHandle(dtoType, writeMethod); - this.scalarType = typeManager.type(propertyType(writeMethod), propertyClass(writeMethod)); + Field field = findField(dtoType, name); + DeployProperty deployProp = new DtoMetaDeployProperty(name, + dtoType, + propertyType(writeMethod), + propertyClass(writeMethod), + field == null ? Collections.emptySet() : AnnotationUtil.metaFindAllFor(field, annotationFilter), + writeMethod); + scalarType = getScalarType(typeManager, deployProp); } else { this.scalarType = null; this.setter = null; } } + private ScalarType getScalarType(TypeManager typeManager, DeployProperty deployProp) { + final ScalarType scalarType; + + List json = deployProp.getMetaAnnotations(DbJson.class); + if (!json.isEmpty()) { + return typeManager.dbJsonType(deployProp, DeployUtil.dbJsonStorage(json.get(0).storage()), json.get(0).length()); + } + List jsonB = deployProp.getMetaAnnotations(DbJsonB.class); + if (!jsonB.isEmpty()) { + return typeManager.dbJsonType(deployProp, DbPlatformType.JSONB, jsonB.get(0).length()); + } + if (typeManager.jsonMarkerAnnotation() != null + && !deployProp.getMetaAnnotations(typeManager.jsonMarkerAnnotation()).isEmpty()) { + return typeManager.dbJsonType(deployProp, DbPlatformType.JSON, 0); + } + return typeManager.type(deployProp); + + + } + + /** + * Find all annotations on fields and methods. + */ + private Set findMetaAnnotations(Class dtoType, Method writeMethod, String name, Set> annotationFilter) { + Field field = findField(dtoType, name); + if (field != null) { + Set metaAnnotations = AnnotationUtil.metaFindAllFor(field, annotationFilter); + metaAnnotations.addAll(AnnotationUtil.metaFindAllFor(writeMethod, annotationFilter)); + return metaAnnotations; + } else { + return AnnotationUtil.metaFindAllFor(writeMethod, annotationFilter); + } + } + + /** + * Find field in class with same name + */ + private Field findField(Class type, String name) { + while (type != Object.class && type != null) { + try { + return dtoType.getDeclaredField(name); + } catch (NoSuchFieldException e) { + type = type.getSuperclass(); + } + } + return null; + } + private static MethodHandle lookupMethodHandle(Class dtoType, Method method) throws NoSuchMethodException, IllegalAccessException { return LOOKUP.findVirtual(dtoType, method.getName(), MethodType.methodType(method.getReturnType(), method.getParameterTypes())); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java index d781b2abc2..773bcca096 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/DefaultTypeManager.java @@ -16,10 +16,12 @@ import io.ebeaninternal.server.core.ServiceUtil; import io.ebeaninternal.server.core.bootup.BootupClasses; import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; +import io.ebeaninternal.server.deploy.meta.DeployProperty; import jakarta.persistence.AttributeConverter; import jakarta.persistence.EnumType; import java.io.File; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; @@ -189,7 +191,8 @@ public ScalarType type(int jdbcType) { } @Override - public ScalarType type(Type propertyType, Class propertyClass) { + public ScalarType type(DeployProperty prop) { + Type propertyType = prop.getGenericType(); if (propertyType instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) propertyType; Type rawType = pt.getRawType(); @@ -197,7 +200,7 @@ public ScalarType type(Type propertyType, Class propertyClass) { return dbArrayType((Class) rawType, propertyType, true); } } - return type(propertyClass); + return type(prop.getPropertyType()); } /** @@ -295,8 +298,14 @@ private boolean isEnumType(Type valueType) { return TypeReflectHelper.isEnumType(valueType); } + @Override - public ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLength) { + public Class jsonMarkerAnnotation() { + return jsonMapper == null ? null : jsonMapper.markerAnnotation(); + } + + @Override + public ScalarType dbJsonType(DeployProperty prop, int dbType, int dbLength) { Class type = prop.getPropertyType(); if (type.equals(String.class)) { return ScalarTypeJsonString.typeFor(postgres, dbType); @@ -326,14 +335,14 @@ public ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLengt return createJsonObjectMapperType(prop, dbType, DocPropertyType.OBJECT); } - private boolean keepSource(DeployBeanProperty prop) { + private boolean keepSource(DeployProperty prop) { if (prop.getMutationDetection() == MutationDetection.DEFAULT) { prop.setMutationDetection(jsonManager.mutationDetection()); } return prop.getMutationDetection() == MutationDetection.SOURCE; } - private DocPropertyType docPropertyType(DeployBeanProperty prop, Class type) { + private DocPropertyType docPropertyType(DeployProperty prop, Class type) { return type.equals(List.class) || type.equals(Set.class) ? docType(prop.getGenericType()) : DocPropertyType.OBJECT; } @@ -367,14 +376,18 @@ private boolean isMapValueTypeObject(Type genericType) { return Object.class.equals(typeArgs[1]) || "?".equals(typeArgs[1].toString()); } - private ScalarType createJsonObjectMapperType(DeployBeanProperty prop, int dbType, DocPropertyType docType) { + private ScalarType createJsonObjectMapperType(DeployProperty prop, int dbType, DocPropertyType docType) { if (jsonMapper == null) { throw new IllegalArgumentException("Unsupported @DbJson mapping - Jackson ObjectMapper not present for " + prop); } if (MutationDetection.DEFAULT == prop.getMutationDetection()) { prop.setMutationDetection(jsonManager.mutationDetection()); } - var req = new ScalarJsonRequest(jsonManager, dbType, docType, prop.getDesc().getBeanType(), prop.getMutationDetection(), prop.getName()); + Class type = prop.getOwnerType(); + if (prop instanceof DeployBeanProperty) { + type = ((DeployBeanProperty) prop).getField().getDeclaringClass(); + } + var req = new ScalarJsonRequest(jsonManager, dbType, docType, type, prop.getMutationDetection(), prop.getName()); return jsonMapper.createType(req); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java index a8b8e0ff26..8ddd36a1f1 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/type/TypeManager.java @@ -1,9 +1,10 @@ package io.ebeaninternal.server.type; import io.ebean.core.type.ScalarType; -import io.ebeaninternal.server.deploy.meta.DeployBeanProperty; +import io.ebeaninternal.server.deploy.meta.DeployProperty; import jakarta.persistence.EnumType; +import java.lang.annotation.Annotation; import java.lang.reflect.Type; /** @@ -40,20 +41,25 @@ public interface TypeManager { *

* For example Array based ScalarType for types like {@code List}. */ - ScalarType type(Type propertyType, Class type); + ScalarType type(DeployProperty property); /** * Create a ScalarType for an Enum using a mapping (rather than JPA Ordinal or String which has limitations). */ ScalarType enumType(Class> enumType, EnumType enumerated); + /** + * Returns the Json Marker annotation (e.g. JacksonAnnotation) + */ + Class jsonMarkerAnnotation(); + /** * Return the ScalarType used to handle JSON content. *

* Note that type expected to be JsonNode or Map. *

*/ - ScalarType dbJsonType(DeployBeanProperty prop, int dbType, int dbLength); + ScalarType dbJsonType(DeployProperty prop, int dbType, int dbLength); /** * Return the ScalarType used to handle DB ARRAY. diff --git a/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java b/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java index 47fbfdc582..4826e3873e 100644 --- a/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java +++ b/ebean-test/src/test/java/org/tests/json/TestDbJson_Jackson.java @@ -1,15 +1,25 @@ package org.tests.json; import com.fasterxml.jackson.databind.ObjectMapper; -import io.ebean.xtest.BaseTestCase; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.ebean.DB; +import io.ebean.annotation.DbJson; +import io.ebean.annotation.DbJsonB; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.Test; +import org.tests.model.json.BasicJacksonType; import org.tests.model.json.EBasicJsonJackson; import org.tests.model.json.EBasicJsonJackson2; import org.tests.model.json.LongJacksonType; import org.tests.model.json.StringJacksonType; import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -59,9 +69,56 @@ public void testJsonDeserializeAnnotation() throws IOException { assertThat(found.getValueMap()).containsEntry(1L, "one").containsEntry(2L, "two"); } + public static class DtoJackson { + @DbJson(length = 700) + Set> valueSet = new LinkedHashSet<>(); + + @DbJsonB + List> valueList = new ArrayList<>(); + + @DbJson(length = 700) + @JsonDeserialize(keyAs = Long.class) + Map> valueMap = new LinkedHashMap<>(); + + @DbJson(length = 500) + BasicJacksonType plainValue; + + public Set> getValueSet() { + return valueSet; + } + + public void setValueSet(Set> valueSet) { + this.valueSet = valueSet; + } + + public List> getValueList() { + return valueList; + } + + public void setValueList(List> valueList) { + this.valueList = valueList; + } + + public Map> getValueMap() { + return valueMap; + } + + public void setValueMap(Map> valueMap) { + this.valueMap = valueMap; + } + + public BasicJacksonType getPlainValue() { + return plainValue; + } + + public void setPlainValue(BasicJacksonType plainValue) { + this.plainValue = plainValue; + } + } + /** * This testcase verifies if polymorph objects will work in ebean. - * + *

* for BasicJacksonType there exists two types and has a @JsonTypeInfo * annotation. It is expected that this information is also honored by ebean. */ @@ -94,7 +151,8 @@ public void testPolymorph() throws IOException { assertThat(found.getPlainValue()).isInstanceOf(LongJacksonType.class); assertThat(found.getValueList()).hasSize(2); assertThat(found.getValueSet()).hasSize(2); - assertThat(found.getValueMap()).hasSize(2);; + assertThat(found.getValueMap()).hasSize(2); + ; DB.save(bean); @@ -103,7 +161,16 @@ public void testPolymorph() throws IOException { assertThat(found.getPlainValue()).isInstanceOf(LongJacksonType.class); assertThat(found.getValueList()).hasSize(2); assertThat(found.getValueSet()).hasSize(2); - assertThat(found.getValueMap()).hasSize(2);; + assertThat(found.getValueMap()).hasSize(2); + + + DtoJackson dto = DB.find(EBasicJsonJackson2.class).setId(bean.getId()) + .select("valueSet,valueList,valueMap,plainValue").asDto(DtoJackson.class).findOne(); + + assertThat(dto.getPlainValue()).isInstanceOf(LongJacksonType.class); + assertThat(dto.getValueList()).hasSize(2); + assertThat(dto.getValueSet()).hasSize(2); + assertThat(dto.getValueMap()).hasSize(2); } From 1370ec9c450b97e155831bc30124a2bca4b97592 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 15:10:42 +0200 Subject: [PATCH 09/36] NEW: QueryPlanLogger for DB2 Squash to DB2 QueryPlan --- .../main/java/io/ebean/DatabaseBuilder.java | 10 + .../java/io/ebean/config/DatabaseConfig.java | 17 ++ .../server/core/InternalConfiguration.java | 2 + .../server/query/QueryPlanLoggerDb2.java | 122 ++++++++++ .../server/query/QueryPlanLoggerDb2.sql | 211 ++++++++++++++++++ 5 files changed, 362 insertions(+) create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/query/QueryPlanLoggerDb2.java create mode 100644 ebean-core/src/main/resources/io/ebeaninternal/server/query/QueryPlanLoggerDb2.sql diff --git a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java index e0269c26b8..483cc95da4 100644 --- a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java +++ b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java @@ -2085,6 +2085,11 @@ default DatabaseBuilder queryPlanEnable(boolean queryPlanEnable) { @Deprecated DatabaseBuilder setQueryPlanEnable(boolean queryPlanEnable); + /** + * Set platform specific query plan options. + */ + DatabaseBuilder queryPlanOptions(String queryPlanOptions); + /** * Set the query plan collection threshold in microseconds. *

@@ -3072,6 +3077,11 @@ interface Settings extends DatabaseBuilder { */ boolean isQueryPlanEnable(); + /** + * Returns platform specific query plan options. + */ + String getQueryPlanOptions(); + /** * Return the query plan collection threshold in microseconds. */ diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java index 5fbab9a1f9..ed284b3bed 100644 --- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java +++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java @@ -513,6 +513,11 @@ public class DatabaseConfig implements DatabaseBuilder.Settings { */ private boolean queryPlanEnable; + /** + * Additional platform specific options for query-plan generation. + */ + private String queryPlanOptions; + /** * The default threshold in micros for collecting query plans. */ @@ -2168,6 +2173,7 @@ protected void loadSettings(PropertiesWrapper p) { queryPlanTTLSeconds = p.getInt("queryPlanTTLSeconds", queryPlanTTLSeconds); slowQueryMillis = p.getLong("slowQueryMillis", slowQueryMillis); queryPlanEnable = p.getBoolean("queryPlan.enable", queryPlanEnable); + queryPlanOptions = p.get("queryPlan.options", queryPlanOptions); queryPlanThresholdMicros = p.getLong("queryPlan.thresholdMicros", queryPlanThresholdMicros); queryPlanCapture = p.getBoolean("queryPlan.capture", queryPlanCapture); queryPlanCapturePeriodSecs = p.getLong("queryPlan.capturePeriodSecs", queryPlanCapturePeriodSecs); @@ -2492,6 +2498,17 @@ public DatabaseConfig setQueryPlanEnable(boolean queryPlanEnable) { return this; } + @Override + public String getQueryPlanOptions() { + return queryPlanOptions; + } + + @Override + public DatabaseConfig queryPlanOptions(String queryPlanOptions) { + this.queryPlanOptions = queryPlanOptions; + return this; + } + @Override public long getQueryPlanThresholdMicros() { return queryPlanThresholdMicros; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index ecbb7f29e2..411352b38e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -596,6 +596,8 @@ QueryPlanLogger queryPlanLogger(Platform platform) { return new QueryPlanLoggerSqlServer(); case ORACLE: return new QueryPlanLoggerOracle(); + case DB2: + return new QueryPlanLoggerDb2(config.getQueryPlanOptions()); case POSTGRES: return new QueryPlanLoggerExplain("explain (analyze, buffers) "); case YUGABYTE: diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/QueryPlanLoggerDb2.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/QueryPlanLoggerDb2.java new file mode 100644 index 0000000000..568b9df5c5 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/QueryPlanLoggerDb2.java @@ -0,0 +1,122 @@ + +package io.ebeaninternal.server.query; + +import io.ebean.util.IOUtils; +import io.ebean.util.StringHelper; +import io.ebeaninternal.api.CoreLog; +import io.ebeaninternal.api.SpiDbQueryPlan; +import io.ebeaninternal.api.SpiQueryPlan; +import io.ebeaninternal.server.bind.capture.BindCapture; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Map; +import java.util.Random; + +import static java.lang.System.Logger.Level.WARNING; + +/** + * A QueryPlanLogger for DB2. + *

+ * To use query plan capturing, you have to install the explain tables with + * SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA ). + * To do this in a repeatable script, you may use this statement: + * + *

+ * BEGIN
+ * IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = CURRENT SCHEMA AND TABNAME = 'EXPLAIN_STREAM') THEN
+ *    call SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA );
+ * END IF;
+ * END
+ * 
+ * + * @author Roland Praml, FOCONIS AG + */ +public final class QueryPlanLoggerDb2 extends QueryPlanLogger { + + private Random rnd = new Random(); + + private final String schema; + + private final boolean create; + + private static final String GET_PLAN_TEMPLATE = readReasource("QueryPlanLoggerDb2.sql"); + + private static final String CREATE_TEMPLATE = "BEGIN\n" + + "IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = ${SCHEMA} AND TABNAME = 'EXPLAIN_STREAM') THEN\n" + + " CALL SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', ${SCHEMA} );\n" + + "END IF;\n" + + "END"; + + public QueryPlanLoggerDb2(String opts) { + Map map = StringHelper.delimitedToMap(opts, ";", "="); + create = !"false" .equals(map.get("create")); // default is create + String schema = map.get("schema"); // should be null or SYSTOOLS + if (schema == null || schema.isEmpty()) { + this.schema = null; + } else { + this.schema = schema.toUpperCase(); + } + } + + private static String readReasource(String resName) { + try (InputStream stream = QueryPlanLoggerDb2.class.getResourceAsStream(resName)) { + if (stream == null) { + throw new IllegalStateException("Could not find resource " + resName); + } + BufferedReader reader = IOUtils.newReader(stream); + StringBuilder sb = new StringBuilder(); + reader.lines().forEach(line -> sb.append(line).append('\n')); + return sb.toString(); + } catch (IOException e) { + throw new IllegalStateException("Could not read resource " + resName, e); + } + } + + @Override + public SpiDbQueryPlan collectPlan(Connection conn, SpiQueryPlan plan, BindCapture bind) { + try (Statement stmt = conn.createStatement()) { + if (create) { + // create explain tables if neccessary + if (schema == null) { + stmt.execute(CREATE_TEMPLATE.replace("${SCHEMA}", "CURRENT USER")); + } else { + stmt.execute(CREATE_TEMPLATE.replace("${SCHEMA}", "'" + schema + "'")); + } + conn.commit(); + } + + try { + int queryNo = rnd.nextInt(Integer.MAX_VALUE); + + String sql = "EXPLAIN PLAN SET QUERYNO = " + queryNo + " FOR " + plan.sql(); + try (PreparedStatement explainStmt = conn.prepareStatement(sql)) { + bind.prepare(explainStmt, conn); + explainStmt.execute(); + } + + sql = schema == null + ? GET_PLAN_TEMPLATE.replace("${SCHEMA}", conn.getMetaData().getUserName().toUpperCase()) + : GET_PLAN_TEMPLATE.replace("${SCHEMA}", schema); + + try (PreparedStatement pstmt = conn.prepareStatement(sql)) { + pstmt.setInt(1, queryNo); + try (ResultSet rset = pstmt.executeQuery()) { + return readQueryPlan(plan, bind, rset); + } + } + } finally { + conn.rollback(); // do not keep query plans in DB + } + } catch (SQLException e) { + CoreLog.log.log(WARNING, "Could not log query plan", e); + return null; + } + } +} diff --git a/ebean-core/src/main/resources/io/ebeaninternal/server/query/QueryPlanLoggerDb2.sql b/ebean-core/src/main/resources/io/ebeaninternal/server/query/QueryPlanLoggerDb2.sql new file mode 100644 index 0000000000..ae3993082c --- /dev/null +++ b/ebean-core/src/main/resources/io/ebeaninternal/server/query/QueryPlanLoggerDb2.sql @@ -0,0 +1,211 @@ +WITH tree(operator_ID, level, path, explain_time, cycle) +AS +( +SELECT 1 operator_id + , 0 level + , CAST('001' AS VARCHAR(1000)) path + , max(explain_time) explain_time + , 0 + FROM ${SCHEMA}.EXPLAIN_OPERATOR O + WHERE O.EXPLAIN_REQUESTER = SESSION_USER + +UNION ALL + +SELECT s.source_id + , level + 1 + , tree.path || '/' || LPAD(CAST(s.source_id AS VARCHAR(3)), 3, '0') path + , tree.explain_time + , POSITION('/' || LPAD(CAST(s.source_id AS VARCHAR(3)), 3, '0') || '/' IN path USING OCTETS) + FROM tree + , ${SCHEMA}.EXPLAIN_STREAM S + WHERE s.target_id = tree.operator_id + AND s.explain_time = tree.explain_time + AND S.Object_Name IS NULL + AND S.explain_requester = SESSION_USER + AND tree.cycle = 0 + AND level < 100 +) +SELECT * + FROM ( +SELECT "Explain Plan" + FROM ( +SELECT CAST( LPAD(id, MAX(LENGTH(id)) OVER(), ' ') + || ' | ' + || RPAD(operation, MAX(LENGTH(operation)) OVER(), ' ') + || ' | ' + || LPAD(rows, MAX(LENGTH(rows)) OVER(), ' ') + || ' | ' + -- Don't show ActualRows columns if there are no actuals available at all + || CASE WHEN COUNT(ActualRows) OVER () > 1 -- the heading 'ActualRows' is always present, so "1" means no OTHER values + THEN LPAD(ActualRows, MAX(LENGTH(ActualRows)) OVER(), ' ') || ' | ' + ELSE '' + END + || LPAD(cost, MAX(LENGTH(cost)) OVER(), ' ') + AS VARCHAR(100)) "Explain Plan" + , path + FROM ( +SELECT 'ID' ID + , 'Operation' Operation + , 'Rows' Rows + , 'ActualRows' ActualRows + , 'Cost' Cost + , '0' Path + FROM SYSIBM.SYSDUMMY1 +-- TODO: UNION ALL yields duplicate. where do they come from? +UNION +SELECT CAST(tree.operator_id as VARCHAR(254)) ID + , CAST(LPAD(' ', tree.level, ' ') + || CASE WHEN tree.cycle = 1 + THEN '(cycle) ' + ELSE '' + END + || COALESCE ( + TRIM(O.Operator_Type) + || COALESCE(' (' || argument || ')', '') + || ' ' + || COALESCE(S.Object_Name,'') + , '' + ) + AS VARCHAR(254)) AS OPERATION + , COALESCE(CAST(rows AS VARCHAR(254)), '') Rows + , CAST(ActualRows as VARCHAR(254)) ActualRows -- note: no coalesce + , COALESCE(CAST(CAST(O.Total_Cost AS BIGINT) AS VARCHAR(254)), '') Cost + , path + FROM tree + LEFT JOIN ( SELECT i.source_id + , i.target_id + , CAST(CAST(ROUND(o.stream_count) AS BIGINT) AS VARCHAR(12)) + || ' of ' + || CAST (total_rows AS VARCHAR(12)) + || CASE WHEN total_rows > 0 + AND ROUND(o.stream_count) <= total_rows THEN + ' (' + || LPAD(CAST (ROUND(ROUND(o.stream_count)/total_rows*100,2) + AS NUMERIC(5,2)), 6, ' ') + || '%)' + ELSE '' + END rows + , CASE WHEN act.actual_value is not null then + CAST(CAST(ROUND(act.actual_value) AS BIGINT) AS VARCHAR(12)) + || ' of ' + || CAST (total_rows AS VARCHAR(12)) + || CASE WHEN total_rows > 0 THEN + ' (' + || LPAD(CAST (ROUND(ROUND(act.actual_value)/total_rows*100,2) + AS NUMERIC(5,2)), 6, ' ') + || '%)' + ELSE NULL + END END ActualRows + , i.object_name + , i.explain_time + FROM (SELECT MAX(source_id) source_id + , target_id + , MIN(CAST(ROUND(stream_count,0) AS BIGINT)) total_rows + , CAST(LISTAGG(object_name) AS VARCHAR(50)) object_name + , explain_time + FROM ${SCHEMA}.EXPLAIN_STREAM + WHERE explain_time = (SELECT MAX(explain_time) + FROM ${SCHEMA}.EXPLAIN_OPERATOR + WHERE EXPLAIN_REQUESTER = SESSION_USER + ) + GROUP BY target_id, explain_time + ) I + LEFT JOIN ${SCHEMA}.EXPLAIN_STREAM O + ON ( I.target_id=o.source_id + AND I.explain_time = o.explain_time + AND O.EXPLAIN_REQUESTER = SESSION_USER + ) + LEFT JOIN ${SCHEMA}.EXPLAIN_ACTUALS act + ON ( act.operator_id = i.target_id + AND act.explain_time = i.explain_time + AND act.explain_requester = SESSION_USER + AND act.ACTUAL_TYPE like 'CARDINALITY%' + ) + ) s + ON ( s.target_id = tree.operator_id + AND s.explain_time = tree.explain_time + ) + LEFT JOIN ${SCHEMA}.EXPLAIN_OPERATOR O + ON ( o.operator_id = tree.operator_id + AND o.explain_time = tree.explain_time + AND o.explain_requester = SESSION_USER + ) + LEFT JOIN (SELECT LISTAGG (CASE argument_type + WHEN 'UNIQUE' THEN + CASE WHEN argument_value = 'TRUE' + THEN 'UNIQUE' + ELSE NULL + END + WHEN 'TRUNCSRT' THEN + CASE WHEN argument_value = 'TRUE' + THEN 'TOP-N' + ELSE NULL + END + WHEN 'SCANDIR' THEN + CASE WHEN argument_value != 'FORWARD' + THEN argument_value + ELSE NULL + END + ELSE argument_value + END + , ' ') argument + , operator_id + , explain_time + FROM ${SCHEMA}.EXPLAIN_ARGUMENT EA + WHERE argument_type IN ('AGGMODE' -- GRPBY + , 'UNIQUE', 'TRUNCSRT' -- SORT + , 'SCANDIR' -- IXSCAN, TBSCAN + , 'OUTERJN' -- JOINs + ) + AND explain_requester = SESSION_USER + GROUP BY explain_time, operator_id + + ) A + ON ( a.operator_id = tree.operator_id + AND a.explain_time = tree.explain_time + ) + ) O +UNION ALL +VALUES ('Explain plan (c) 2014-2017 by Markus Winand - NO WARRANTY - V20171102','Z0') + , ('Modifications by Ember Crooks - NO WARRANTY','Z1') + , ('http://use-the-index-luke.com/s/last_explained','Z2') + , ('', 'A') + , ('', 'Y') + , ('Predicate Information', 'AA') +UNION ALL +SELECT CAST (LPAD(CASE WHEN operator_id = LAG (operator_id) + OVER (PARTITION BY operator_id + ORDER BY pred_order + ) + THEN '' + ELSE operator_id || ' - ' + END + , MAX(LENGTH(operator_id )+4) OVER() + , ' ') + || how_applied + || ' ' + || predicate_text + AS VARCHAR(100)) "Predicate Information" + , 'P' || LPAD(id_order, 5, '0') || pred_order path + FROM (SELECT CAST(operator_id AS VARCHAR(254)) operator_id + , LPAD(trim(how_applied) + , MAX (LENGTH(TRIM(how_applied))) + OVER (PARTITION BY operator_id) + , ' ' + ) how_applied + -- next: capped to length 80 to avoid + -- SQL0445W Value "..." has been truncated. SQLSTATE=01004 + -- error when long literal values may appear (space padded!) + , CAST(substr(predicate_text, 1, 80) AS VARCHAR(80)) predicate_text + , CASE how_applied WHEN 'START' THEN '1' + WHEN 'STOP' THEN '2' + WHEN 'SARG' THEN '3' + ELSE '9' + END pred_order + , operator_id id_order + FROM ${SCHEMA}.EXPLAIN_PREDICATE p + WHERE explain_time = (SELECT max(explain_time) FROM ${SCHEMA}.EXPLAIN_STATEMENT WHERE queryno = ?) + ) +) +ORDER BY path +) From e8138c8c1ef91b66424556ad3c71acab3057e0dc Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 15:13:43 +0200 Subject: [PATCH 10/36] DDL generation can be invoked separately --- .../src/main/java/io/ebean/Database.java | 4 ++++ .../io/ebeaninternal/api/SpiDdlGenerator.java | 5 ++++ .../server/core/DefaultServer.java | 5 ++++ .../server/core/InternalConfiguration.java | 5 ++++ .../dbmigration/DdlGenerator.java | 24 +++++++++---------- .../ebean/xtest/internal/api/TDSpiServer.java | 5 ++++ 6 files changed, 35 insertions(+), 13 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/Database.java b/ebean-api/src/main/java/io/ebean/Database.java index 202c347e67..d0d747d8e3 100644 --- a/ebean-api/src/main/java/io/ebean/Database.java +++ b/ebean-api/src/main/java/io/ebean/Database.java @@ -1529,4 +1529,8 @@ default Set checkUniqueness(Object bean, Transaction transaction) { */ void truncate(Class... beanTypes); + /** + * RunDdl manually. This can be used if 'db.ddl.run=false' is set and you plan to run DDL manually. + */ + void runDdl(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java index b3c4858139..d821e75b84 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiDdlGenerator.java @@ -12,4 +12,9 @@ public interface SpiDdlGenerator { */ void execute(boolean online); + /** + * Run DDL manually. This can be used to initialize multi tenant environments or if you plan not to run + * DDL on startup + */ + void runDdl(); } 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 b876b0f916..6631d3f99c 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 @@ -2322,4 +2322,9 @@ List queryPlanInit(QueryPlanInit initRequest) { List queryPlanCollectNow(QueryPlanRequest request) { return queryPlanManager.collect(request); } + + @Override + public void runDdl() { + ddlGenerator.runDdl(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java index 411352b38e..57d4f840b9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java @@ -622,6 +622,11 @@ private static class NoopDdl implements SpiDdlGenerator { this.ddlRun = ddlRun; } + @Override + public void runDdl() { + CoreLog.log.log(ERROR, "Manual DDL run not possible"); + } + @Override public void execute(boolean online) { if (online && ddlRun) { diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java index 9fb81acf1e..a4e8233ea4 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DdlGenerator.java @@ -63,11 +63,10 @@ public DdlGenerator(SpiEbeanServer server) { if (!config.getTenantMode().isDdlEnabled() && config.isDdlRun()) { log.log(WARNING, "DDL can''t be run on startup with TenantMode " + config.getTenantMode()); this.runDdl = false; - this.useMigrationStoredProcedures = false; } else { this.runDdl = config.isDdlRun(); - this.useMigrationStoredProcedures = config.getDatabasePlatform().useMigrationStoredProcedures(); } + this.useMigrationStoredProcedures = config.getDatabasePlatform() != null && config.getDatabasePlatform().useMigrationStoredProcedures(); this.scriptTransform = createScriptTransform(config); this.baseDir = initBaseDir(); } @@ -85,7 +84,7 @@ private File initBaseDir() { @Override public void execute(boolean online) { generateDdl(); - if (online) { + if (online && runDdl) { runDdl(); } } @@ -105,16 +104,15 @@ protected void generateDdl() { /** * Run the DDL drop and DDL create scripts if properties have been set. */ - protected void runDdl() { - if (runDdl) { - Connection connection = null; - try { - connection = obtainConnection(); - runDdlWith(connection); - } finally { - JdbcClose.rollback(connection); - JdbcClose.close(connection); - } + @Override + public void runDdl() { + Connection connection = null; + try { + connection = obtainConnection(); + runDdlWith(connection); + } finally { + JdbcClose.rollback(connection); + JdbcClose.close(connection); } } diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java index 20d8856150..a16f2835fd 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java @@ -627,4 +627,9 @@ public void loadBeanL2(EntityBeanIntercept ebi) { public void loadBean(EntityBeanIntercept ebi) { } + + @Override + public void runDdl() { + + } } From 313d1092a4de4e5acb0319e22c73d3a54adf423d Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Mon, 7 Oct 2024 15:15:02 +0200 Subject: [PATCH 11/36] NEW: DefaultDbMigration is configurable from properties --- .../dbmigration/DbMigrationPlugin.java | 56 ++++++++ .../dbmigration/DefaultDbMigration.java | 131 ++++++++++++++---- .../src/main/java/module-info.java | 1 + .../META-INF/services/io.ebean.plugin.Plugin | 1 + .../resources/application-test.properties | 24 ---- .../DbMigrationDropHistoryTest.java | 2 - .../dbmigration/DbMigrationGenerateTest.java | 42 ++---- .../src/test/resources/ebean.properties | 17 +++ 8 files changed, 196 insertions(+), 78 deletions(-) create mode 100644 ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java create mode 100644 ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java new file mode 100644 index 0000000000..1839cf4917 --- /dev/null +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DbMigrationPlugin.java @@ -0,0 +1,56 @@ +package io.ebeaninternal.dbmigration; + +import java.io.IOException; + +import io.ebean.plugin.Plugin; +import io.ebean.plugin.SpiServer; + +/** + * Plugin to generate db-migration scripts automatically. + * @author Roland Praml, FOCONIS AG + */ +public class DbMigrationPlugin implements Plugin { + + private DefaultDbMigration dbMigration; + + private static String lastMigration; + private static String lastInit; + + @Override + public void configure(SpiServer server) { + dbMigration = new DefaultDbMigration(); + dbMigration.setServer(server); + } + + @Override + public void online(boolean online) { + try { + lastInit = null; + lastMigration = null; + if (dbMigration.generate) { + String tmp = lastMigration = dbMigration.generateMigration(); + if (tmp == null) { + return; + } + } + if (dbMigration.generateInit) { + lastInit = dbMigration.generateInitMigration(); + } + } catch (IOException e) { + throw new RuntimeException("Error while generating migration", e); + } + } + + @Override + public void shutdown() { + dbMigration = null; + } + + public static String getLastInit() { + return lastInit; + } + + public static String getLastMigration() { + return lastMigration; + } +} diff --git a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java index c44c6da5f4..0338c5c36f 100644 --- a/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java +++ b/ebean-ddl-generator/src/main/java/io/ebeaninternal/dbmigration/DefaultDbMigration.java @@ -1,6 +1,7 @@ package io.ebeaninternal.dbmigration; import io.avaje.applog.AppLog; +import io.avaje.classpath.scanner.core.Location; import io.ebean.DB; import io.ebean.Database; import io.ebean.DatabaseBuilder; @@ -10,6 +11,7 @@ import io.ebean.config.dbplatform.DatabasePlatformProvider; import io.ebean.dbmigration.DbMigration; import io.ebean.util.IOUtils; +import io.ebean.util.StringHelper; import io.ebeaninternal.api.DbOffline; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.dbmigration.ddlgeneration.DdlOptions; @@ -24,10 +26,7 @@ import java.io.File; import java.io.IOException; import java.io.Writer; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; -import java.util.ServiceLoader; +import java.util.*; import static io.ebeaninternal.api.PlatformMatch.matchPlatform; import static java.lang.System.Logger.Level.*; @@ -59,8 +58,8 @@ public class DefaultDbMigration implements DbMigration { private static final String initialVersion = "1.0"; private static final String GENERATED_COMMENT = "THIS IS A GENERATED FILE - DO NOT MODIFY"; - private final List platformProviders = new ArrayList<>(); - protected final boolean online; + private List platformProviders = new ArrayList<>(); + protected boolean online; private boolean logToSystemOut = true; protected SpiEbeanServer server; protected String pathToResources = "src/main/resources"; @@ -75,8 +74,10 @@ public class DefaultDbMigration implements DbMigration { protected List platforms = new ArrayList<>(); protected DatabaseBuilder.Settings databaseBuilder; protected DbConstraintNaming constraintNaming; + @Deprecated protected Boolean strictMode; - protected Boolean includeGeneratedFileComment; + protected boolean includeGeneratedFileComment; + @Deprecated protected String header; protected String applyPrefix = ""; protected String version; @@ -86,6 +87,9 @@ public class DefaultDbMigration implements DbMigration { private int lockTimeoutSeconds; protected boolean includeBuiltInPartitioning = true; protected boolean includeIndex; + protected boolean generate = false; + protected boolean generateInit = false; + private boolean keepLastInit = true; /** * Create for offline migration generation. @@ -122,12 +126,66 @@ public void setServerConfig(DatabaseBuilder builder) { if (constraintNaming == null) { this.constraintNaming = databaseBuilder.getConstraintNaming(); } + if (databasePlatform == null) { + this.databasePlatform = databaseBuilder.getDatabasePlatform(); + } Properties properties = config.getProperties(); if (properties != null) { - PropertiesWrapper props = new PropertiesWrapper("ebean", config.getName(), properties, null); + PropertiesWrapper props = new PropertiesWrapper("ebean", config.getName(), properties, config.getClassLoadConfig()); migrationPath = props.get("migration.migrationPath", migrationPath); migrationInitPath = props.get("migration.migrationInitPath", migrationInitPath); pathToResources = props.get("migration.pathToResources", pathToResources); + addForeignKeySkipCheck = props.getBoolean("migration.addForeignKeySkipCheck", addForeignKeySkipCheck); + applyPrefix = props.get("migration.applyPrefix", applyPrefix); + databasePlatform = props.createInstance(DatabasePlatform.class, "migration.databasePlatform", databasePlatform); + generatePendingDrop = props.get("migration.generatePendingDrop", generatePendingDrop); + includeBuiltInPartitioning = props.getBoolean("migration.includeBuiltInPartitioning", includeBuiltInPartitioning); + includeGeneratedFileComment = props.getBoolean("migration.includeGeneratedFileComment", includeGeneratedFileComment); + includeIndex = props.getBoolean("migration.includeIndex", includeIndex); + lockTimeoutSeconds = props.getInt("migration.lockTimeoutSeconds", lockTimeoutSeconds); + logToSystemOut = props.getBoolean("migration.logToSystemOut", logToSystemOut); + modelPath = props.get("migration.modelPath", modelPath); + modelSuffix = props.get("migration.modelSuffix", modelSuffix); + name = props.get("migration.name", name); + online = props.getBoolean("migration.online", online); + vanillaPlatform = props.getBoolean("migration.vanillaPlatform", vanillaPlatform); + version = props.get("migration.version", version); + generate = props.getBoolean("migration.generate", generate); + generateInit = props.getBoolean("migration.generateInit", generateInit); + // header & strictMode must be configured at DatabaseConfig level + parsePlatforms(props, config); + } + } + + protected void parsePlatforms(PropertiesWrapper props, DatabaseBuilder.Settings config) { + String platforms = props.get("migration.platforms"); + if (platforms == null || platforms.isEmpty()) { + return; + } + String[] tmp = StringHelper.splitNames(platforms); + for (String plat : tmp) { + DatabasePlatform dbPlatform; + String platformName = plat; + String platformPrefix = null; + int pos = plat.indexOf('='); + if (pos != -1) { + platformName = plat.substring(0, pos); + platformPrefix = plat.substring(pos + 1); + } + + if (platformName.indexOf('.') == -1) { + // parse platform as enum value + Platform platform = Enum.valueOf(Platform.class, platformName.toUpperCase()); + dbPlatform = platform(platform); + } else { + // parse platform as class + dbPlatform = (DatabasePlatform) config.getClassLoadConfig().newInstance(platformName); + } + if (platformPrefix == null) { + platformPrefix = dbPlatform.platform().name().toLowerCase(); + } + + addDatabasePlatform(dbPlatform, platformPrefix); } } @@ -318,7 +376,18 @@ private String generateMigrationFor(boolean initMigration) throws IOException { } String pendingVersion = generatePendingDrop(); - if (pendingVersion != null) { + if ("auto".equals(pendingVersion)) { + StringJoiner sj = new StringJoiner(","); + String diff = generateDiff(request); + if (diff != null) { + sj.add(diff); + request = createRequest(initMigration); + } + for (String pendingDrop : request.getPendingDrops()) { + sj.add(generatePendingDrop(request, pendingDrop)); + } + return sj.length() == 0 ? null : sj.toString(); + } else if (pendingVersion != null) { return generatePendingDrop(request, pendingVersion); } else { return generateDiff(request); @@ -553,7 +622,7 @@ private String generateMigration(Request request, Migration dbMigration, String return null; } else { if (!platforms.isEmpty()) { - writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir); + writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir, request.initMigration && keepLastInit); } else if (databasePlatform != null) { // writer needs the current model to provide table/column details for @@ -633,12 +702,17 @@ private String toUnderScore(String name) { /** * Write any extra platform ddl. */ - private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException { + private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath, boolean clear) throws IOException { DdlOptions options = new DdlOptions(addForeignKeySkipCheck); for (Pair pair : platforms) { DdlWrite writer = new DdlWrite(new MConfiguration(), currentModel.read(), options); PlatformDdlWriter platformWriter = createDdlWriter(pair.platform); File subPath = platformWriter.subPath(writePath, pair.prefix); + if (clear) { + for (File existing : subPath.listFiles()) { + existing.delete(); + } + } platformWriter.processMigration(dbMigration, writer, subPath, fullVersion); } } @@ -656,7 +730,7 @@ private boolean writeMigrationXml(Migration dbMigration, File resourcePath, Stri if (file.exists()) { return false; } - String comment = Boolean.TRUE.equals(includeGeneratedFileComment) ? GENERATED_COMMENT : null; + String comment = includeGeneratedFileComment ? GENERATED_COMMENT : null; MigrationXmlWriter xmlWriter = new MigrationXmlWriter(comment); xmlWriter.write(dbMigration, file); return true; @@ -674,11 +748,14 @@ private void setDefaults() { databasePlatform = server.databasePlatform(); } if (databaseBuilder != null) { + // FIXME: StrictMode and header may be defined HERE and in DatabaseConfig. + // We shoild change either DefaultDbMigration or databaseConfig, so that it is only + // defined on one place if (strictMode != null) { - databaseBuilder.setDdlStrictMode(strictMode); + databaseBuilder.ddlStrictMode(strictMode); } if (header != null) { - databaseBuilder.setDdlHeader(header); + databaseBuilder.ddlHeader(header); } } } @@ -748,15 +825,20 @@ public File migrationDirectory() { * Return the file path to write the xml and sql to. */ File migrationDirectory(boolean initMigration) { - // path to src/main/resources in typical maven project - File resourceRootDir = new File(pathToResources); - if (!resourceRootDir.exists()) { - String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath()); - throw new UnknownResourcePathException(msg); - } - String resourcePath = migrationPath(initMigration); + Location resourcePath = migrationPath(initMigration); // expect to be a path to something like - src/main/resources/dbmigration - File path = new File(resourceRootDir, resourcePath); + File path; + if (resourcePath.isClassPath()) { + // path to src/main/resources in typical maven project + File resourceRootDir = new File(pathToResources); + if (!resourceRootDir.exists()) { + String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath()); + throw new UnknownResourcePathException(msg); + } + path = new File(resourceRootDir, resourcePath.path()); + } else { + path = new File(resourcePath.path()); + } if (!path.exists()) { if (!path.mkdirs()) { logInfo("Warning - Unable to ensure migration directory exists at %s", path.getAbsolutePath()); @@ -765,8 +847,9 @@ File migrationDirectory(boolean initMigration) { return path; } - private String migrationPath(boolean initMigration) { - return initMigration ? migrationInitPath : migrationPath; + private Location migrationPath(boolean initMigration) { + // remove classpath: or filesystem: prefix + return new Location(initMigration ? migrationInitPath : migrationPath); } /** diff --git a/ebean-ddl-generator/src/main/java/module-info.java b/ebean-ddl-generator/src/main/java/module-info.java index 50a8c62311..47e9d4aa78 100644 --- a/ebean-ddl-generator/src/main/java/module-info.java +++ b/ebean-ddl-generator/src/main/java/module-info.java @@ -1,5 +1,6 @@ module io.ebean.ddl.generator { + uses io.ebean.plugin.Plugin; exports io.ebean.dbmigration; provides io.ebean.dbmigration.DbMigration with io.ebeaninternal.dbmigration.DefaultDbMigration; diff --git a/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin b/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin new file mode 100644 index 0000000000..83ec94df48 --- /dev/null +++ b/ebean-ddl-generator/src/main/resources/META-INF/services/io.ebean.plugin.Plugin @@ -0,0 +1 @@ +io.ebeaninternal.dbmigration.DbMigrationPlugin diff --git a/ebean-ddl-generator/src/test/resources/application-test.properties b/ebean-ddl-generator/src/test/resources/application-test.properties index dfcb052ab0..64e7f78869 100644 --- a/ebean-ddl-generator/src/test/resources/application-test.properties +++ b/ebean-ddl-generator/src/test/resources/application-test.properties @@ -10,27 +10,3 @@ datasource.h2.url=jdbc:h2:mem:h2AutoTune datasource.pg.username=sa datasource.pg.password= datasource.pg.url=jdbc:h2:mem:h2AutoTune - -# parameters for migration test -datasource.migrationtest.username=SA -datasource.migrationtest.password=SA -datasource.migrationtest.url=jdbc:h2:mem:migration -ebean.migrationtest.applyPrefix=V -ebean.migrationtest.ddl.generate=false -ebean.migrationtest.ddl.run=false -ebean.migrationtest.ddl.header=-- Migrationscripts for ebean unittest -ebean.migrationtest.migration.appName=migrationtest -ebean.migrationtest.migration.migrationPath=dbmigration/migrationtest -ebean.migrationtest.migration.strict=true - -# parameters for migration test -datasource.migrationtest-history.username=SA -datasource.migrationtest-history.password=SA -datasource.migrationtest-history.url=jdbc:h2:mem:migration -ebean.migrationtest-history.applyPrefix=V -ebean.migrationtest-history.ddl.generate=false -ebean.migrationtest-history.ddl.run=false -ebean.migrationtest-history.ddl.header=-- Migrationscripts for ebean unittest DbMigrationDropHistoryTest -ebean.migrationtest-history.migration.appName=migrationtest-history -ebean.migrationtest-history.migration.migrationPath=dbmigration/migrationtest-history -ebean.migrationtest-history.migration.strict=true diff --git a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java index f1657ca272..b6cb57149d 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationDropHistoryTest.java @@ -80,13 +80,11 @@ public static void main(String[] args) throws IOException { List pendingDrops = migration.getPendingDrops(); assertThat(pendingDrops).contains("1.1"); - //System.setProperty("ddl.migration.pendingDropsFor", "1.1"); migration.setGeneratePendingDrop("1.1"); assertThat(migration.generateMigration()).isEqualTo("1.2__dropsFor_1.1"); assertThatThrownBy(()->migration.generateMigration()) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("No 'pendingDrops'"); // subsequent call - System.clearProperty("ddl.migration.pendingDropsFor"); server.shutdown(); logger.info("end"); diff --git a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java index 6d19a976b6..3328cfb417 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/dbmigration/DbMigrationGenerateTest.java @@ -5,6 +5,8 @@ import io.ebean.annotation.Platform; import io.ebean.DatabaseBuilder; import io.ebean.config.DatabaseConfig; +import io.ebeaninternal.api.DbOffline; + import io.ebean.dbmigration.DbMigration; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -18,7 +20,6 @@ import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** @@ -97,7 +98,7 @@ public static void run(String pathToResources) throws IOException { config.getProperties().put("ebean.hana.generateUniqueDdl", "true"); // need to generate unique statements to prevent them from being filtered out as duplicates by the DdlRunner config.setPackages(Arrays.asList("misc.migration.v1_0")); - Database server = DatabaseFactory.create(config); + Database server = createServer(config); migration.setServer(server); // then we generate migration scripts for v1_0 @@ -108,43 +109,28 @@ public static void run(String pathToResources) throws IOException { // and now for v1_1 config.setPackages(Arrays.asList("misc.migration.v1_1")); server.shutdown(); - server = DatabaseFactory.create(config); + server = createServer(config); migration.setServer(server); - assertThat(migration.generateMigration()).isEqualTo("1.1"); - assertThat(migration.generateMigration()).isNull(); // subsequent call - - - - System.setProperty("ddl.migration.pendingDropsFor", "1.1"); - assertThat(migration.generateMigration()).isEqualTo("1.2__dropsFor_1.1"); - - assertThatThrownBy(()->migration.generateMigration()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("No 'pendingDrops'"); // subsequent call - - System.clearProperty("ddl.migration.pendingDropsFor"); + assertThat(migration.generateMigration()).isEqualTo("1.1,1.2__dropsFor_1.1"); assertThat(migration.generateMigration()).isNull(); // subsequent call // and now for v1_2 with config.setPackages(Arrays.asList("misc.migration.v1_2")); server.shutdown(); - server = DatabaseFactory.create(config); + server = createServer(config); migration.setServer(server); - assertThat(migration.generateMigration()).isEqualTo("1.3"); - assertThat(migration.generateMigration()).isNull(); // subsequent call - - - System.setProperty("ddl.migration.pendingDropsFor", "1.3"); - assertThat(migration.generateMigration()).isEqualTo("1.4__dropsFor_1.3"); - assertThatThrownBy(migration::generateMigration) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("No 'pendingDrops'"); // subsequent call - - System.clearProperty("ddl.migration.pendingDropsFor"); + assertThat(migration.generateMigration()).isEqualTo("1.3,1.4__dropsFor_1.3"); assertThat(migration.generateMigration()).isNull(); // subsequent call server.shutdown(); logger.info("end"); } + private static Database createServer(DatabaseConfig config) { + DbOffline.setGenerateMigration(); + Database server = DatabaseFactory.create(config); + DbOffline.reset(); + return server; + } + } diff --git a/ebean-test/src/test/resources/ebean.properties b/ebean-test/src/test/resources/ebean.properties index 6e3485c75b..444b2ad0a2 100644 --- a/ebean-test/src/test/resources/ebean.properties +++ b/ebean-test/src/test/resources/ebean.properties @@ -199,6 +199,7 @@ datasource.hana.username=EBEAN_TEST datasource.hana.password=Eb3an_test datasource.hana.url=jdbc:sap://hxehost:39013/?databaseName=HXE #datasource.hana.driver=com.sap.db.jdbc.Driver +# # parameters for migration test datasource.migrationtest.username=SA @@ -210,6 +211,21 @@ ebean.migrationtest.ddl.run=false ebean.migrationtest.ddl.header=-- Migrationscripts for ebean unittest ebean.migrationtest.migration.appName=migrationtest ebean.migrationtest.migration.migrationPath=migrationtest/dbmigration +ebean.migrationtest.migration.migrationInitPath=migrationtest/dbinit +ebean.migrationtest.migration.strict=true +ebean.migrationtest.migration.generate=true +ebean.migrationtest.migration.run=false +ebean.migrationtest.migration.includeIndex=true +ebean.migrationtest.migration.generateInit=true +ebean.migrationtest.migration.generatePendingDrop=auto +ebean.migrationtest.migration.platforms=db2luw,h2,hsqldb,mysql,mysql55,mariadb,postgres,oracle,sqlite,sqlserver17,hana,yugabyte +#migration.migrationtest.db2luw.prefix=db2 +#migration.migrationtest.sqlserver17.prefix=sqlserver +dbmigration.platform.mariadb.useMigrationStoredProcedures=true +dbmigration.platform.mysql.useMigrationStoredProcedures=true + + + ebean.migrationtest.migration.strict=true # enable stored procedures f dbmigration.platform.mariadb.useMigrationStoredProcedures=true @@ -225,4 +241,5 @@ ebean.migrationtest-history.ddl.run=false ebean.migrationtest-history.ddl.header=-- Migrationscripts for ebean unittest DbMigrationDropHistoryTest ebean.migrationtest-history.migration.appName=migrationtest-history ebean.migrationtest-history.migration.migrationPath=migrationtest-history/dbmigration +ebean.migrationtest-history.migration.migrationInitPath=migrationtest-history/dbinit ebean.migrationtest-history.migration.strict=true From 62ea9ecbf384f2d25ae04d612dbb29e88bb4ad62 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 12:04:08 +0200 Subject: [PATCH 12/36] NOPR - CustomDeployParser base code Squash to customDeployParser --- .../main/java/io/ebean/DatabaseBuilder.java | 11 ++++ .../java/io/ebean/config/DatabaseConfig.java | 19 ++++++ .../io/ebean/plugin/CustomDeployParser.java | 15 +++++ .../plugin/DeployBeanDescriptorMeta.java | 36 +++++++++++ .../plugin/DeployBeanPropertyAssocMeta.java | 23 +++++++ .../ebean/plugin/DeployBeanPropertyMeta.java | 37 +++++++++++ .../server/core/DefaultContainer.java | 1 + .../server/core/bootup/BootupClasses.java | 16 +++++ .../server/deploy/BeanDescriptorManager.java | 3 + .../deploy/CustomDeployParserManager.java | 23 +++++++ .../deploy/meta/DeployBeanDescriptor.java | 18 +++++- .../deploy/meta/DeployBeanProperty.java | 9 ++- .../deploy/meta/DeployBeanPropertyAssoc.java | 9 ++- .../tevent/CustomFormulaAnnotationParser.java | 61 +++++++++++++++++++ .../org/tests/model/tevent/TEventOne.java | 9 +++ .../aggregation/TestAggregationCount.java | 2 +- 16 files changed, 287 insertions(+), 5 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java create mode 100644 ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java create mode 100644 ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java create mode 100644 ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/deploy/CustomDeployParserManager.java create mode 100644 ebean-test/src/test/java/org/tests/model/tevent/CustomFormulaAnnotationParser.java diff --git a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java index 483cc95da4..174550ab32 100644 --- a/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java +++ b/ebean-api/src/main/java/io/ebean/DatabaseBuilder.java @@ -15,6 +15,7 @@ import io.ebean.event.changelog.ChangeLogRegister; import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditPrepare; +import io.ebean.plugin.CustomDeployParser; import jakarta.persistence.EnumType; import javax.sql.DataSource; @@ -1833,6 +1834,11 @@ default DatabaseBuilder resourceDirectory(String resourceDirectory) { */ DatabaseBuilder addServerConfigStartup(ServerConfigStartup configStartupListener); + /** + * Add a CustomDeployParser. + */ + DatabaseConfig addCustomDeployParser(CustomDeployParser customDeployParser); + /** * Register all the BeanPersistListener instances. *

@@ -2991,6 +2997,11 @@ interface Settings extends DatabaseBuilder { */ List getServerConfigStartupListeners(); + /** + * Returns the registered CustomDeployParsers. + */ + List getCustomDeployParsers(); + /** * Return the default PersistenceContextScope to be used if one is not explicitly set on a query. *

diff --git a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java index ed284b3bed..510f18716a 100644 --- a/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java +++ b/ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java @@ -19,6 +19,7 @@ import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditPrepare; import io.ebean.meta.MetricNamingMatch; +import io.ebean.plugin.CustomDeployParser; import io.ebean.util.StringHelper; import jakarta.persistence.EnumType; @@ -417,6 +418,7 @@ public class DatabaseConfig implements DatabaseBuilder.Settings { private List queryAdapters = new ArrayList<>(); private final List bulkTableEventListeners = new ArrayList<>(); private final List configStartupListeners = new ArrayList<>(); + private final List customDeployParsers = new ArrayList<>(); /** * By default inserts are included in the change log. @@ -2034,6 +2036,23 @@ public List getServerConfigStartupListeners() { return configStartupListeners; } + @Override + public DatabaseConfig addCustomDeployParser(CustomDeployParser customDeployParser) { + customDeployParsers.add(customDeployParser); + return this; + } + + @Override + public List getCustomDeployParsers() { + return customDeployParsers; + } + + /** + * Register all the BeanPersistListener instances. + *

+ * Note alternatively you can use {@link #add(BeanPersistListener)} to add + * BeanPersistListener instances one at a time. + */ @Override public DatabaseConfig setPersistListeners(List persistListeners) { this.persistListeners = persistListeners; diff --git a/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java b/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java new file mode 100644 index 0000000000..4c854f2f1e --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/CustomDeployParser.java @@ -0,0 +1,15 @@ +package io.ebean.plugin; + +import io.ebean.config.dbplatform.DatabasePlatform; + +/** + * Fired after all beans are parsed. You may implement own parsers to handle custom annotations. + * (See test case for example) + * + * @author Roland Praml, FOCONIS AG + */ +@FunctionalInterface +public interface CustomDeployParser { + + void parse(DeployBeanDescriptorMeta descriptor, DatabasePlatform databasePlatform); +} diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java new file mode 100644 index 0000000000..4347a5057c --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanDescriptorMeta.java @@ -0,0 +1,36 @@ +package io.ebean.plugin; + +import java.util.Collection; + +/** + * General deployment information. This is used in {@link CustomDeployParser}. + * + * @author Roland Praml, FOCONIS AG + */ +public interface DeployBeanDescriptorMeta { + + /** + * Return a collection of all BeanProperty deployment information. + */ + public Collection propertiesAll(); + + /** + * Get a BeanProperty by its name. + */ + public DeployBeanPropertyMeta getBeanProperty(String secondaryBeanName); + + /** + * Return the DeployBeanDescriptorMeta for the given bean class. + */ + public DeployBeanDescriptorMeta getDeployBeanDescriptorMeta(Class propertyType); + + /** + * Returns the discriminator column, if any. + * @return + */ + public String getDiscriminatorColumn(); + + public String getBaseTable(); + + DeployBeanPropertyMeta idProperty(); +} diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java new file mode 100644 index 0000000000..d7d1d637a3 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyAssocMeta.java @@ -0,0 +1,23 @@ +package io.ebean.plugin; + +public interface DeployBeanPropertyAssocMeta extends DeployBeanPropertyMeta { + + /** + * Return the mappedBy deployment attribute. + *

+ * This is the name of the property in the 'detail' bean that maps back to + * this 'master' bean. + *

+ */ + String getMappedBy(); + + /** + * Return the base table for this association. + *

+ * This has the table name which is used to determine the relationship for + * this association. + *

+ */ + String getBaseTable(); + +} diff --git a/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java new file mode 100644 index 0000000000..4ba2d3e2a1 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/plugin/DeployBeanPropertyMeta.java @@ -0,0 +1,37 @@ +package io.ebean.plugin; + +import java.lang.reflect.Field; + +public interface DeployBeanPropertyMeta { + + /** + * Return the name of the property. + */ + String getName(); + + /** + * The database column name this is mapped to. + */ + String getDbColumn(); + + /** + * Return the bean Field associated with this property. + */ + Field getField(); + + /** + * The property is based on a formula. + */ + void setSqlFormula(String sqlSelect, String sqlJoin); + + /** + * Return the bean type. + */ + Class getOwningType(); + + /** + * Return the property type. + */ + Class getPropertyType(); + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java index c0f7174489..91896abcfb 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DefaultContainer.java @@ -181,6 +181,7 @@ private BootupClasses bootupClasses(DatabaseBuilder.Settings config) { bootup.addFindControllers(config.getFindControllers()); bootup.addPersistListeners(config.getPersistListeners()); bootup.addQueryAdapters(config.getQueryAdapters()); + bootup.addCustomDeployParser(config.getCustomDeployParsers()); bootup.addChangeLogInstances(config); return bootup; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java index 726411dd2f..57b6afd04a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java @@ -11,6 +11,7 @@ import io.ebean.event.changelog.ChangeLogRegister; import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditPrepare; +import io.ebean.plugin.CustomDeployParser; import io.ebean.util.AnnotationUtil; import io.ebeaninternal.api.CoreLog; @@ -54,6 +55,7 @@ public class BootupClasses implements Predicate> { private final List> beanPersistListenerCandidates = new ArrayList<>(); private final List> beanQueryAdapterCandidates = new ArrayList<>(); private final List> serverConfigStartupCandidates = new ArrayList<>(); + private final List> customDeployParserCandidates = new ArrayList<>(); private final List idGeneratorInstances = new ArrayList<>(); private final List beanPersistControllerInstances = new ArrayList<>(); @@ -63,6 +65,7 @@ public class BootupClasses implements Predicate> { private final List beanPersistListenerInstances = new ArrayList<>(); private final List beanQueryAdapterInstances = new ArrayList<>(); private final List serverConfigStartupInstances = new ArrayList<>(); + private final List customDeployParserInstances = new ArrayList<>(); // single objects private Class changeLogPrepareClass; @@ -172,6 +175,10 @@ public void addServerConfigStartup(List startupInstances) { add(startupInstances, serverConfigStartupInstances, serverConfigStartupCandidates); } + public void addCustomDeployParser(List customDeployParser) { + add(customDeployParser, customDeployParserInstances, customDeployParserCandidates); + } + public void addChangeLogInstances(DatabaseBuilder.Settings config) { readAuditPrepare = config.getReadAuditPrepare(); readAuditLogger = config.getReadAuditLogger(); @@ -288,6 +295,10 @@ public List getBeanQueryAdapters() { return createAdd(beanQueryAdapterInstances, beanQueryAdapterCandidates); } + public List getCustomDeployParsers() { + return createAdd(customDeployParserInstances, customDeployParserCandidates); + } + /** * Return the list of Embeddable classes. */ @@ -408,6 +419,11 @@ private boolean isInterestingInterface(Class cls) { interesting = true; } + if (CustomDeployParser.class.isAssignableFrom(cls)) { + customDeployParserCandidates.add((Class) cls); + interesting = true; + } + // single instances, last assigned wins if (ChangeLogListener.class.isAssignableFrom(cls)) { changeLogListenerClass = (Class) cls; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index 2cee486469..c14932c9a8 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -76,6 +76,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy private final BeanFinderManager beanFinderManager; private final PersistListenerManager persistListenerManager; private final BeanQueryAdapterManager beanQueryAdapterManager; + private final CustomDeployParserManager customDeployParserManager; private final NamingConvention namingConvention; private final DeployCreateProperties createProperties; private final BeanManagerFactory beanManagerFactory; @@ -156,6 +157,7 @@ public BeanDescriptorManager(InternalConfiguration config) { this.persistListenerManager = new PersistListenerManager(bootupClasses); this.beanQueryAdapterManager = new BeanQueryAdapterManager(bootupClasses); this.beanFinderManager = new BeanFinderManager(bootupClasses); + this.customDeployParserManager = new CustomDeployParserManager(bootupClasses); this.transientProperties = new TransientProperties(); this.changeLogPrepare = config.changeLogPrepare(bootupClasses.getChangeLogPrepare()); this.changeLogListener = config.changeLogListener(bootupClasses.getChangeLogListener()); @@ -307,6 +309,7 @@ public Map deploy(List mappings) { readEntityBeanTable(); readEntityDeploymentAssociations(); readInheritedIdGenerators(); + deployInfoMap.values().forEach(customDeployParserManager::parse); // creates the BeanDescriptors readEntityRelationships(); List> list = new ArrayList<>(descMap.values()); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/CustomDeployParserManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/CustomDeployParserManager.java new file mode 100644 index 0000000000..93a86d98b7 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/CustomDeployParserManager.java @@ -0,0 +1,23 @@ +package io.ebeaninternal.server.deploy; + +import java.util.List; + +import io.ebean.plugin.CustomDeployParser; +import io.ebeaninternal.server.core.bootup.BootupClasses; +import io.ebeaninternal.server.deploy.parse.DeployBeanInfo; + +public class CustomDeployParserManager { + + private final List parsers; + + public CustomDeployParserManager(BootupClasses bootupClasses) { + parsers = bootupClasses.getCustomDeployParsers(); + } + + public void parse(DeployBeanInfo value) { + for (CustomDeployParser parser : parsers) { + parser.parse(value.getDescriptor(), value.getUtil().dbPlatform()); + } + } + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index f96cabb465..fe5b6192c5 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -10,6 +10,7 @@ import io.ebean.config.dbplatform.PlatformIdGenerator; import io.ebean.event.*; import io.ebean.event.changelog.ChangeLogFilter; +import io.ebean.plugin.DeployBeanDescriptorMeta; import io.ebean.text.PathProperties; import io.ebean.util.SplitName; import io.ebeaninternal.api.ConcurrencyMode; @@ -30,7 +31,7 @@ /** * Describes Beans including their deployment information. */ -public class DeployBeanDescriptor { +public class DeployBeanDescriptor implements DeployBeanDescriptorMeta { private static final Map EMPTY_NAMED_QUERY = new HashMap<>(); @@ -192,7 +193,7 @@ public TableJoin getPrimaryKeyJoin() { /** * Return the DeployBeanInfo for the given bean class. */ - DeployBeanInfo getDeploy(Class cls) { + public DeployBeanInfo getDeploy(Class cls) { return manager.deploy(cls); } @@ -598,6 +599,7 @@ public String[] getDependentTables() { * Return the base table. Only properties mapped to the base table are by * default persisted. */ + @Override public String getBaseTable() { return baseTable; } @@ -675,6 +677,7 @@ public Collection properties() { /** * Get a BeanProperty by its name. */ + @Override public DeployBeanProperty getBeanProperty(String propName) { return propMap.get(propName); } @@ -794,6 +797,7 @@ public String toString() { /** * Return a collection of all BeanProperty deployment information. */ + @Override public Collection propertiesAll() { return propMap.values(); } @@ -863,6 +867,7 @@ public String getSinglePrimaryKeyColumn() { /** * Return the BeanProperty that is the Id. */ + @Override public DeployBeanProperty idProperty() { if (idProperty != null) { return idProperty; @@ -1107,4 +1112,13 @@ private String getDeployWord(String expression) { } } + @Override + public String getDiscriminatorColumn() { + return inheritInfo == null ? null : inheritInfo.getDiscriminatorColumn(); + } + + @Override + public DeployBeanDescriptorMeta getDeployBeanDescriptorMeta(Class propertyType) { + return getDeploy(propertyType).getDescriptor(); + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java index fa96d959ea..cbcee8328c 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanProperty.java @@ -8,6 +8,7 @@ import io.ebean.config.dbplatform.DbEncryptFunction; import io.ebean.config.dbplatform.ExtraDbTypes; import io.ebean.core.type.ScalarType; +import io.ebean.plugin.DeployBeanPropertyMeta; import io.ebean.util.AnnotationUtil; import io.ebeaninternal.server.core.InternString; import io.ebeaninternal.server.deploy.BeanProperty; @@ -38,7 +39,7 @@ * Description of a property of a bean. Includes its deployment information such * as database column mapping information. */ -public class DeployBeanProperty implements DeployProperty { +public class DeployBeanProperty implements DeployProperty, DeployBeanPropertyMeta { private static final int ID_ORDER = 1000000; private static final int UNIDIRECTIONAL_ORDER = 100000; @@ -410,6 +411,7 @@ public void setOwningType(Class owningType) { this.owningType = owningType; } + @Override public Class getOwningType() { return owningType; } @@ -438,6 +440,7 @@ public void setSetter(BeanPropertySetter setter) { /** * Return the name of the property. */ + @Override public String getName() { return name; } @@ -452,6 +455,7 @@ public void setName(String name) { /** * Return the bean Field associated with this property. */ + @Override public Field getField() { return field; } @@ -556,6 +560,7 @@ public String getSqlFormulaJoin() { /** * The property is based on a formula. */ + @Override public void setSqlFormula(String formulaSelect, String formulaJoin) { this.sqlFormulaSelect = formulaSelect; this.sqlFormulaJoin = formulaJoin.isEmpty() ? null : formulaJoin; @@ -659,6 +664,7 @@ public String getElPlaceHolder() { /** * The database column name this is mapped to. */ + @Override public String getDbColumn() { if (sqlFormulaSelect != null) { return sqlFormulaSelect; @@ -852,6 +858,7 @@ public void setTransient() { /** * Return the property type. */ + @Override public Class getPropertyType() { return propertyType; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java index 3461be3bd8..123972cb0d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssoc.java @@ -1,5 +1,6 @@ package io.ebeaninternal.server.deploy.meta; +import io.ebean.plugin.DeployBeanPropertyAssocMeta; import io.ebeaninternal.server.deploy.BeanCascadeInfo; import io.ebeaninternal.server.deploy.BeanTable; import io.ebeaninternal.server.deploy.PropertyForeignKey; @@ -7,7 +8,7 @@ /** * Abstract base for properties mapped to an associated bean, list, set or map. */ -public abstract class DeployBeanPropertyAssoc extends DeployBeanProperty { +public abstract class DeployBeanPropertyAssoc extends DeployBeanProperty implements DeployBeanPropertyAssocMeta { /** * The type of the joined bean. @@ -129,6 +130,7 @@ public PropertyForeignKey getForeignKey() { * this 'master' bean. *

*/ + @Override public String getMappedBy() { return mappedBy; } @@ -173,4 +175,9 @@ public void setFetchPreference(int fetchPreference) { public void setTargetType(Class targetType) { this.targetType = (Class)targetType; } + + @Override + public String getBaseTable() { + return getBeanTable().getBaseTable(); + } } diff --git a/ebean-test/src/test/java/org/tests/model/tevent/CustomFormulaAnnotationParser.java b/ebean-test/src/test/java/org/tests/model/tevent/CustomFormulaAnnotationParser.java new file mode 100644 index 0000000000..3f392aed06 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/tevent/CustomFormulaAnnotationParser.java @@ -0,0 +1,61 @@ +package org.tests.model.tevent; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import io.ebean.annotation.Formula; +import io.ebean.config.dbplatform.DatabasePlatform; +import io.ebean.plugin.CustomDeployParser; +import io.ebean.plugin.DeployBeanDescriptorMeta; +import io.ebean.plugin.DeployBeanPropertyMeta; +import io.ebean.util.AnnotationUtil; +import io.ebeaninternal.server.deploy.meta.DeployBeanPropertyAssocMany; + +/** + * Custom Annotation parser which parses @Count annotation + * + * @author Roland Praml, FOCONIS AG + */ +public class CustomFormulaAnnotationParser implements CustomDeployParser { + + private int counter; + + + @Target(FIELD) + @Retention(RUNTIME) + @Formula(select="TODO", join = "TODO") // meta-formula + public @interface Count { + String value(); + } + + + + @Override + public void parse(final DeployBeanDescriptorMeta descriptor, final DatabasePlatform databasePlatform) { + for (DeployBeanPropertyMeta prop : descriptor.propertiesAll()) { + readField(descriptor, prop); + } + } + + private void readField(DeployBeanDescriptorMeta descriptor, DeployBeanPropertyMeta prop) { + Count countAnnot = AnnotationUtil.get(prop.getField(), Count.class); + if (countAnnot != null) { + // @Count found, so build the (complex) count formula + DeployBeanPropertyAssocMany countProp = (DeployBeanPropertyAssocMany) descriptor.getBeanProperty(countAnnot.value()); + counter++; + String tmpTable = "f"+counter; + String sqlSelect = "coalesce(" + tmpTable + ".child_count, 0)"; + String parentId = countProp.getMappedBy() + "_id"; + String tableName = countProp.getBeanTable().getBaseTable(); + String sqlJoin = "left join (select " + parentId +", count(*) as child_count from " + tableName + " GROUP BY " + parentId + " )" + + " " + tmpTable + " on " + tmpTable + "." +parentId + " = ${ta}." + descriptor.idProperty().getDbColumn(); + prop.setSqlFormula(sqlSelect, sqlJoin); +// prop.setSqlFormula("f1.child_count", +// "join (select parent_id, count(*) as child_count from child_entity GROUP BY parent_id) f1 on f1.parent_id = ${ta}.id"); + } + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java b/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java index 286ad84e2f..5896f84480 100644 --- a/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java +++ b/ebean-test/src/test/java/org/tests/model/tevent/TEventOne.java @@ -42,6 +42,11 @@ public enum Status { @OneToMany(mappedBy = "event", cascade = CascadeType.ALL) List logs; + @CustomFormulaAnnotationParser.Count("logs") + //@Formula(select = "f1.child_count", + //join = "left join (select event_id, count(*) as child_count from tevent_many GROUP BY event_id ) as f1 on f1.event_id = ${ta}.id") + Long customFormula; + public TEventOne(String name, Status status) { this.name = name; this.status = status; @@ -64,6 +69,10 @@ public Long getCount() { return count; } + public Long getCustomFormula() { + return customFormula; + } + public BigDecimal getTotalUnits() { return totalUnits; } diff --git a/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java b/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java index 7d2bc67791..52e7d2237a 100644 --- a/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java +++ b/ebean-test/src/test/java/org/tests/query/aggregation/TestAggregationCount.java @@ -49,7 +49,7 @@ public void testBaseSelect() { List list = query.findList(); String sql = sqlOf(query, 5); - assertThat(sql).contains("select t0.id, t0.name, t0.status, t0.version, t0.event_id from tevent_one t0"); + assertThat(sql).contains("select t0.id, t0.name, t0.status, coalesce(f1.child_count, 0), t0.version, t0.event_id from tevent_one t0"); for (TEventOne eventOne : list) { // lazy loading on Aggregation properties From 9ebb56a73f5a7d45cc63bd1fdb53c2ab8e6647f2 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 12:06:09 +0200 Subject: [PATCH 13/36] NOPR - make BeanProperty.field public --- .../main/java/io/ebeaninternal/server/deploy/BeanProperty.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index def5457d45..1c9696f4a3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -1036,7 +1036,7 @@ public List dbMigrationInfos() { /** * Return the bean Field associated with this property. */ - private Field field() { + public Field field() { return field; } From c2f8546a4b5516e8c0933d098232f5cca88fee84 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 11:54:24 +0200 Subject: [PATCH 14/36] NOPR - Do not throw BeanHasBeenDeleted on PrimaryKeyJoinColumn --- .../server/deploy/BeanProperty.java | 7 + .../server/deploy/BeanPropertyAssocOne.java | 20 +- .../deploy/parse/AnnotationAssocOnes.java | 2 +- .../server/persist/DefaultPersister.java | 5 +- .../lazyforeignkeys/MainEntityRelation.java | 15 ++ .../lazyforeignkeys/TestLazyForeignKeys.java | 20 +- .../org/tests/model/onetoone/OtoBMaster.java | 2 +- .../org/tests/model/onetoone/OtoUBPrime.java | 2 +- .../tests/model/onetoone/OtoUBPrimeExtra.java | 2 +- .../org/tests/model/onetoone/OtoUPrime.java | 24 ++- .../tests/model/onetoone/OtoUPrimeExtra.java | 3 +- .../OtoUPrimeExtraWithConstraint.java | 52 +++++ .../onetoone/OtoUPrimeOptionalExtra.java | 61 ++++++ .../onetoone/OtoUPrimeWithConstraint.java | 63 ++++++ .../TestOneToOneImportedPkNative.java | 2 +- .../TestOneToOnePrimaryKeyJoinBidi.java | 2 +- .../TestOneToOnePrimaryKeyJoinOptional.java | 204 ++++++++++++++++-- .../query/other/TestQuerySingleAttribute.java | 6 + 18 files changed, 462 insertions(+), 30 deletions(-) create mode 100644 ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtraWithConstraint.java create mode 100644 ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeOptionalExtra.java create mode 100644 ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeWithConstraint.java diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 1c9696f4a3..feeec9e07b 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -905,6 +905,13 @@ public boolean isImportedPrimaryKey() { return importedPrimaryKey; } + /** + * If true, this property references O2O with its primary key. + */ + public boolean isPrimaryKeyExport() { + return false; + } + @Override public boolean isAssocMany() { // Returns false - override in BeanPropertyAssocMany. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index 9bed5d98ba..be53395af0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -363,6 +363,11 @@ public boolean isOrphanRemoval() { return orphanRemoval; } + @Override + public boolean isPrimaryKeyExport() { + return primaryKeyExport; + } + @Override public void diff(String prefix, Map map, EntityBean newBean, EntityBean oldBean) { Object newEmb = (newBean == null) ? null : getValue(newBean); @@ -600,13 +605,20 @@ private ExportedProperty findMatch(boolean embeddedProp, BeanProperty prop) { return findMatch(embeddedProp, prop, prop.dbColumn(), tableJoin); } + /** + * If column is a primaryKeyExport colum, we can directly use our own ID and do not need to add a join if the relation is not optional + */ + boolean requiresJoin() { + return !primaryKeyExport || isNullable(); + } + @Override public void appendSelect(DbSqlContext ctx, boolean subQuery) { if (!isTransient) { - if (primaryKeyExport) { - descriptor.idProperty().appendSelect(ctx, subQuery); - } else { + if (requiresJoin()) { localHelp.appendSelect(ctx, subQuery); + } else { + descriptor.idProperty().appendSelect(ctx, subQuery); } } } @@ -624,7 +636,7 @@ public SqlJoinType addJoin(SqlJoinType joinType, String a1, String a2, DbSqlCont @Override public void appendFrom(DbSqlContext ctx, SqlJoinType joinType, String manyWhere) { - if (!isTransient && !primaryKeyExport) { + if (!isTransient && requiresJoin()) { localHelp.appendFrom(ctx, joinType); if (sqlFormulaJoin != null) { String alias = ctx.tableAliasManyWhere(manyWhere); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java index ad454afd05..4bf0066c7a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/AnnotationAssocOnes.java @@ -122,7 +122,7 @@ private void readAssocOne(DeployBeanPropertyAssocOne prop) { } } - prop.setJoinType(prop.isNullable()); + prop.setJoinType(prop.isNullable() || prop.getForeignKey() != null && prop.getForeignKey().isNoConstraint()); if (!prop.getTableJoin().hasJoinColumns() && beanTable != null) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index c87d9e65b3..99faaaf355 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -1026,7 +1026,10 @@ private void deleteAssocMany(PersistRequestBean request) { for (BeanPropertyAssocOne prop : expOnes) { // for soft delete check cascade type also supports soft delete if (deleteMode.isHard() || prop.isTargetSoftDelete()) { - if (request.isLoadedProperty(prop)) { + if (prop.isPrimaryKeyExport()) { + // we can delete by id, neither if property loaded or not + delete(prop.targetDescriptor(), prop.descriptor().id(parentBean), t, deleteMode); + } else if (request.isLoadedProperty(prop)) { Object detailBean = prop.getValue(parentBean); if (detailBean != null) { deleteRecurse((EntityBean) detailBean, t, deleteMode); diff --git a/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java b/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java index b20698a7f7..805bcabc9e 100644 --- a/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java +++ b/ebean-test/src/test/java/org/tests/lazyforeignkeys/MainEntityRelation.java @@ -1,6 +1,7 @@ package org.tests.lazyforeignkeys; import io.ebean.annotation.DbForeignKey; +import io.ebean.annotation.NotNull; import org.tests.model.basic.Cat; import jakarta.persistence.*; @@ -29,6 +30,12 @@ public class MainEntityRelation { @DbForeignKey(noConstraint = true) private Cat cat; + @ManyToOne + @NotNull + @JoinColumn(name = "cat2_id") + @DbForeignKey(noConstraint = true) + private Cat cat2; + private String attr1; public MainEntity getEntity1() { @@ -55,6 +62,14 @@ public void setCat(Cat cat) { this.cat = cat; } + public Cat getCat2() { + return cat2; + } + + public void setCat2(Cat cat2) { + this.cat2 = cat2; + } + public String getAttr1() { return attr1; } diff --git a/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java b/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java index f88f148297..d2611e5068 100644 --- a/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java +++ b/ebean-test/src/test/java/org/tests/lazyforeignkeys/TestLazyForeignKeys.java @@ -10,9 +10,11 @@ import org.junit.jupiter.api.Test; import org.tests.model.basic.Cat; +import jakarta.persistence.EntityNotFoundException; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.*; public class TestLazyForeignKeys extends BaseTestCase { @@ -32,6 +34,10 @@ public void prepare() { rel1.setEntity1(e1); rel1.setEntity2(e2); + + Cat cat = new Cat(); + cat.setId(4711L); + rel1.setCat2(cat); DB.save(rel1); } @@ -57,13 +63,25 @@ public void testFindOne() throws Exception { List sql = LoggedSql.stop(); assertThat(sql).hasSize(3); - assertSql(sql.get(0)).contains("select t0.id, t0.attr1, t0.id1, t0.id2, t1.species, t0.cat_id from main_entity_relation t0 left join animal t1 on t1.id = t0.cat_id"); + assertSql(sql.get(0)).contains("select t0.id, t0.attr1, t0.id1, t0.id2, t1.species, t0.cat_id, t2.species, t0.cat2_id " + + "from main_entity_relation t0 left join animal t1 on t1.id = t0.cat_id left join animal t2 on t2.id = t0.cat2_id"); if (isSqlServer() || isOracle()) { assertSql(sql.get(1)).contains("select t0.id, t0.attr1, t0.attr2, CASE WHEN t0.id is null THEN 1 ELSE 0 END from main_entity t0"); } else { assertSql(sql.get(1)).contains("select t0.id, t0.attr1, t0.attr2, t0.id is null from main_entity t0"); assertSql(sql.get(2)).contains("select t0.id, t0.attr1, t0.attr2, t0.id is null from main_entity t0"); } + + assertThat(rel1.getCat2().getId()).isEqualTo(4711L); + assertThatThrownBy(() -> rel1.getCat2().getName()).isInstanceOf(EntityNotFoundException.class); + + Cat cat = new Cat(); + cat.setId(4711L); + cat.setName("miau"); + DB.save(cat); + + DB.refresh(rel1); + assertThat(rel1.getCat2().getName()).isEqualTo("miau"); } @Test diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java index b3e8b3a310..e22cd1cab2 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoBMaster.java @@ -10,7 +10,7 @@ public class OtoBMaster { String name; - @OneToOne(cascade = CascadeType.ALL, mappedBy = "master", fetch = FetchType.LAZY) + @OneToOne(cascade = CascadeType.ALL, mappedBy = "master", fetch = FetchType.LAZY, optional = false) OtoBChild child; public Long getId() { diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java index cfcb4bda40..3ccdc5a437 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrime.java @@ -17,7 +17,7 @@ public class OtoUBPrime { /** * Master side of bi-directional PrimaryJoinColumn. */ - @OneToOne(mappedBy = "prime") + @OneToOne(mappedBy = "prime", optional = false) OtoUBPrimeExtra extra; @Version diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java index eb89a30be7..554d86a67e 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUBPrimeExtra.java @@ -14,7 +14,7 @@ public class OtoUBPrimeExtra { /** * Child side of bi-directional PrimaryJoinColumn. */ - @OneToOne + @OneToOne(optional = false) @PrimaryKeyJoinColumn OtoUBPrime prime; diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java index 620ab44ff9..61614100ac 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrime.java @@ -13,15 +13,27 @@ public class OtoUPrime { String name; + /** * Effectively Ebean automatically sets Cascade PERSIST and mapped by for PrimaryKeyJoinColumn. - * This OneToOne is optional so left join to extra. + * This OneToOne is not optional so use inner join to extra (unless DbForeignkey(noConstraint = true) is set) + * Note: Violating the contract (Storing OtoUPrime without extra) may cause problems: + * - due the inner join, you might not get results from the query + * - you might get a "Beah has been deleted" if lazy load occurs on 'extra' */ - @OneToOne + @OneToOne(orphanRemoval = true, optional = false) @PrimaryKeyJoinColumn + // enforcing left join - without 'noConstraint = true', an inner join is used @DbForeignKey(noConstraint = true) OtoUPrimeExtra extra; + /** + * This OneToOne is optional so left join to extra. + * Setting FetchType.LAZY will NOT add the left join by default to the query. + */ + @OneToOne(mappedBy = "prime", fetch = FetchType.LAZY, orphanRemoval = true, optional = true) + OtoUPrimeOptionalExtra optionalExtra; + @Version Long version; @@ -65,4 +77,12 @@ public Long getVersion() { public void setVersion(Long version) { this.version = version; } + + public OtoUPrimeOptionalExtra getOptionalExtra() { + return optionalExtra; + } + + public void setOptionalExtra(OtoUPrimeOptionalExtra optionalExtra) { + this.optionalExtra = optionalExtra; + } } diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java index 041fc05718..3191624b96 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtra.java @@ -22,7 +22,7 @@ public OtoUPrimeExtra(String extra) { @Override public String toString() { - return "exId:"+ eid +" "+extra; + return "exId:" + eid + " " + extra; } public UUID getEid() { @@ -48,4 +48,5 @@ public Long getVersion() { public void setVersion(Long version) { this.version = version; } + } diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtraWithConstraint.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtraWithConstraint.java new file mode 100644 index 0000000000..cc7e738d29 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeExtraWithConstraint.java @@ -0,0 +1,52 @@ +package org.tests.model.onetoone; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import java.util.UUID; + +@Entity +public class OtoUPrimeExtraWithConstraint { + + @Id + UUID eid; + + String extra; + + @Version + Long version; + + public OtoUPrimeExtraWithConstraint(String extra) { + this.extra = extra; + } + + @Override + public String toString() { + return "exId:" + eid + " " + extra; + } + + public UUID getEid() { + return eid; + } + + public void setEid(UUID eid) { + this.eid = eid; + } + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeOptionalExtra.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeOptionalExtra.java new file mode 100644 index 0000000000..5bb1f70e09 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeOptionalExtra.java @@ -0,0 +1,61 @@ +package org.tests.model.onetoone; + +import jakarta.persistence.*; +import java.util.UUID; + +@Entity +public class OtoUPrimeOptionalExtra { + + @Id + UUID eid; + + String extra; + + @OneToOne(optional = false) + @PrimaryKeyJoinColumn + private OtoUPrime prime; + + @Version + Long version; + + public OtoUPrimeOptionalExtra(String extra) { + this.extra = extra; + } + + @Override + public String toString() { + return "exId:" + eid + " " + extra; + } + + public UUID getEid() { + return eid; + } + + public void setEid(UUID eid) { + this.eid = eid; + } + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public OtoUPrime getPrime() { + return prime; + } + + public void setPrime(OtoUPrime prime) { + this.prime = prime; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeWithConstraint.java b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeWithConstraint.java new file mode 100644 index 0000000000..933afdd68f --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/onetoone/OtoUPrimeWithConstraint.java @@ -0,0 +1,63 @@ +package org.tests.model.onetoone; + +import jakarta.persistence.*; +import java.util.UUID; + +@Entity +public class OtoUPrimeWithConstraint { + + @Id + UUID pid; + + String name; + + @OneToOne(orphanRemoval = true, optional = false) + // @DbForeignKey(noConstraint = true) see OtoUPrime + @PrimaryKeyJoinColumn + OtoUPrimeExtraWithConstraint extra; + + @Version + Long version; + + public OtoUPrimeWithConstraint(String name) { + this.name = name; + } + + @Override + public String toString() { + return "id:" + pid + " name:" + name + " extra:" + extra; + } + + public UUID getPid() { + return pid; + } + + public void setPid(UUID pid) { + this.pid = pid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OtoUPrimeExtraWithConstraint getExtra() { + return extra; + } + + public void setExtra(OtoUPrimeExtraWithConstraint extra) { + this.extra = extra; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java index 122e298e93..1aa765092e 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOneImportedPkNative.java @@ -34,7 +34,7 @@ public void findWithLazyOneToOne() { String sql = sqlOf(query); assertThat(sql).contains("select t0.id, t0.name from oto_bmaster t0 where t0.id "); - assertThat(sql).doesNotContain("left join oto_bchild"); + assertThat(sql).doesNotContain("join oto_bchild"); assertThat(one).isNotNull(); diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java index 81e8905ed7..63dd38bdde 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinBidi.java @@ -45,7 +45,7 @@ public void insertUpdateDelete() { OtoUBPrime oneWith = queryWithFetch.findOne(); assertThat(oneWith).isNotNull(); - assertThat(sqlOf(queryWithFetch, 10)).contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version, t1.eid from oto_ubprime t0 left join oto_ubprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") + assertThat(sqlOf(queryWithFetch, 10)).contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version, t1.eid from oto_ubprime t0 join oto_ubprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") .as("we join to oto_prime_extra"); assertThat(oneWith.getExtra().getExtra()).isEqualTo("v" + desc); diff --git a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java index 39cbfa7309..e458d28e1d 100644 --- a/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java +++ b/ebean-test/src/test/java/org/tests/model/onetoone/TestOneToOnePrimaryKeyJoinOptional.java @@ -1,14 +1,21 @@ package org.tests.model.onetoone; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Query; +import io.ebean.plugin.Property; import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.PersistenceException; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; public class TestOneToOnePrimaryKeyJoinOptional extends BaseTestCase { @@ -21,23 +28,120 @@ private OtoUPrime insert(String desc) { return prime; } - @Test - public void insertWithoutExtra() { + @BeforeEach + void prepare() { + OtoUPrime p1Single = new OtoUPrime("Prime without optional"); + p1Single.setExtra(new OtoUPrimeExtra("Non optional prime required")); + DB.save(p1Single); + + OtoUPrimeExtra p2 = new OtoUPrimeExtra("SinglePrimeExtra"); + try { + DB.save(p2); + fail("PrimExtra cannot exist without Prime"); + } catch (PersistenceException pe) { + + } String desc = "" + System.currentTimeMillis(); OtoUPrime p1 = new OtoUPrime("u" + desc); + p1.setExtra(new OtoUPrimeExtra("u" + desc)); + p1.setOptionalExtra(new OtoUPrimeOptionalExtra("This one has also an optional")); DB.save(p1); + } - Query query = DB.find(OtoUPrime.class) - .setId(p1.getPid()) - .fetch("extra", "eid"); + @AfterEach + void cleanup() { + DB.find(OtoUPrime.class).delete(); + assertThat(DB.find(OtoUPrimeExtra.class).findList()).isEmpty(); + assertThat(DB.find(OtoUPrimeOptionalExtra.class).findList()).isEmpty(); + } - OtoUPrime found = query.findOne(); + public void doTest1(boolean extraFetch, boolean optionalFetch) { + + // Query for "fetch" case - extra bean joined by left join + + Query query1 = DB.find(OtoUPrime.class); + if (extraFetch) { + query1.fetch("extra"); + } + if (optionalFetch) { + query1.fetch("optionalExtra"); + } + List primes = query1.findList(); + if (extraFetch && optionalFetch) { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.version, " + + "t1.eid, t1.extra, t1.version, " + + "t2.eid, t2.extra, t2.version, t2.eid " + + "from oto_uprime t0 " + + "left join oto_uprime_extra t1 on t1.eid = t0.pid " + // left join on non-optional, because DbForeignKey(noConstraint=true) is set + "left join oto_uprime_optional_extra t2 on t2.eid = t0.pid"); // left join on optional + } else if (extraFetch) { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version from oto_uprime t0 left join oto_uprime_extra t1 on t1.eid = t0.pid"); + } else if (optionalFetch) { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.pid, t0.version, t1.eid, t1.extra, t1.version, t1.eid from oto_uprime t0 left join oto_uprime_optional_extra t1 on t1.eid = t0.pid"); + + } else { + assertThat(query1.getGeneratedSql()).isEqualTo("select t0.pid, t0.name, t0.pid, t0.version from oto_uprime t0"); + } - if (found.getExtra() != null) { - found.getExtra().getExtra(); // fails here, because getExtra should be null + List versions = new ArrayList<>(); + for (OtoUPrime prime : primes) { + if (prime.getOptionalExtra() != null) { + versions.add(prime.getOptionalExtra().getVersion()); + } } - assertThat(found.getExtra()).isNull(); + assertThat(primes).hasSize(2); + assertThat(versions).containsExactly(1L); + } + + public void doTest2(boolean withFetch) { + + Query query2 = DB.find(OtoUPrimeOptionalExtra.class); + if (withFetch) { + query2.fetch("prime"); + } + List extraPrimes = query2.findList(); + if (withFetch) { + assertThat(query2.getGeneratedSql()).isEqualTo("select t0.eid, t0.extra, t0.version, t1.pid, t1.name, t1.pid, t1.version from oto_uprime_optional_extra t0 join oto_uprime t1 on t1.pid = t0.eid"); + } else { + assertThat(query2.getGeneratedSql()).isEqualTo("select t0.eid, t0.extra, t0.version, t0.eid from oto_uprime_optional_extra t0"); + } + List versions = new ArrayList<>(); + for (OtoUPrimeOptionalExtra extraPrime : extraPrimes) { + versions.add(extraPrime.getPrime().getVersion()); + } + assertThat(extraPrimes).hasSize(1); + assertThat(versions).containsExactly(1L); + } + + @Test + void testWithExtraFetch1() { + doTest1(true, false); + } + + @Test + void testWithOptionalFetch1() { + doTest1(false, true); + } + + @Test + void testWithBothFetch1() { + doTest1(true, true); + } + + @Test + void testWithoutFetch1() { + doTest1(false, false); + } + + @Test + void testWithFetch2() { + doTest2(true); + } + + @Test + void testWithoutFetch2() { + doTest2(false); } @Test @@ -54,7 +158,7 @@ public void insertUpdateDelete() { OtoUPrime found = query.findOne(); assertThat(found).isNotNull(); - assertThat(sqlOf(query, 4)).contains("select t0.pid, t0.name, t0.version, t0.pid from oto_uprime t0 where t0.pid = ?") + assertThat(sqlOf(query, 4)).contains("select t0.pid, t0.name, t0.pid, t0.version from oto_uprime t0 where t0.pid = ?") .as("we don't join to oto_uprime_extra"); assertThat(found.getName()).isEqualTo("u" + desc); @@ -66,7 +170,8 @@ public void insertUpdateDelete() { OtoUPrime oneWith = queryWithFetch.findOne(); assertThat(oneWith).isNotNull(); - assertThat(sqlOf(queryWithFetch, 6)).contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version from oto_uprime t0 left join oto_uprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") + assertThat(sqlOf(queryWithFetch, 6)) + .contains("select t0.pid, t0.name, t0.version, t1.eid, t1.extra, t1.version from oto_uprime t0 left join oto_uprime_extra t1 on t1.eid = t0.pid where t0.pid = ?") .as("we join to oto_prime_extra"); @@ -98,8 +203,77 @@ private void thenDelete(OtoUPrime found) { DB.delete(bean); List sql = LoggedSql.stop(); - assertThat(sql).hasSize(2); + + assertThat(sql).hasSize(3); assertSql(sql.get(0)).contains("delete from oto_uprime_extra where"); - assertSql(sql.get(1)).contains("delete from oto_uprime where"); + assertSql(sql.get(1)).contains("delete from oto_uprime_optional_extra where"); + assertSql(sql.get(2)).contains("delete from oto_uprime where"); + } + + @Test + void testDdl() { + Collection props = DB.getDefault().pluginApi().beanType(OtoUPrime.class).allProperties(); + + for (Property prop : props) { + System.out.println(prop); + } + } + + @Test + void testContractViolation1() { + + OtoUPrime p1 = new OtoUPrime("Prime having no extra"); + // extra is "optional=false" - and this is a violating of the contract + DB.save(p1); + + Query query = DB.find(OtoUPrime.class).setId(p1.pid); + + OtoUPrime found1 = query.findOne(); + assertThat(query.getGeneratedSql()).doesNotContain("join"); + + assertThat(found1.getExtra()).isNotNull(); + assertThatThrownBy(() -> found1.getExtra().getVersion()).isInstanceOf(EntityNotFoundException.class); + + query.fetch("extra"); + OtoUPrime found2 = query.findOne(); + // Note: We use "left join" here, because 'DbForeignKey(noConstraint=true)' is set oh the property + // if this annotation is not preset, an inner join would be used and 'found2' would be 'null' then + assertThat(query.getGeneratedSql()).contains("from oto_uprime t0 left join oto_uprime_extra"); + assertThat(found2.getExtra()).isNull(); + + } + + @Test + void testContractViolation2() { + + OtoUPrimeExtraWithConstraint p1Const = new OtoUPrimeExtraWithConstraint("test"); + try { + // a foreign key prevents from saving + DB.save(p1Const); + fail("PrimExtra cannot exist without Prime"); + } catch (PersistenceException pe) { + // OK + } + + OtoUPrimeWithConstraint p1 = new OtoUPrimeWithConstraint("Prime having no extra"); + // extra is "optional=false" - and this is a violating of the contract + // Note there is no real foreign key in the database, that would prevent saving this entity + DB.save(p1); + + Query query = DB.find(OtoUPrimeWithConstraint.class).setId(p1.pid); + + OtoUPrimeWithConstraint found1 = query.findOne(); + assertThat(query.getGeneratedSql()).doesNotContain("join"); + + assertThat(found1.getExtra()).isNotNull(); + assertThatThrownBy(() -> found1.getExtra().getVersion()).isInstanceOf(EntityNotFoundException.class); + + query.fetch("extra"); + OtoUPrimeWithConstraint found2 = query.findOne(); + // Note: We use "left join" here, because 'DbForeignKey(noConstraint=true)' is set oh the property + // if this annotation is not preset, an inner join would be used and 'found2' would be 'null' then + assertThat(query.getGeneratedSql()).contains("from oto_uprime_with_constraint t0 join oto_uprime_extra_with_constraint"); + assertThat(found2).isNull(); + } } diff --git a/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java b/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java index 8936626b7d..66f4d9ce42 100644 --- a/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java +++ b/ebean-test/src/test/java/org/tests/query/other/TestQuerySingleAttribute.java @@ -852,19 +852,25 @@ void setup() { e3.setAttr1("a1"); DB.save(e3); + // we need a cat that does not exist in database + Cat cat = DB.reference(Cat.class, 4711); + MainEntityRelation rel = new MainEntityRelation(); rel.setEntity1(e1); rel.setEntity2(e1); + rel.setCat2(cat); DB.save(rel); rel = new MainEntityRelation(); rel.setEntity1(e2); rel.setEntity2(e2); + rel.setCat2(cat); DB.save(rel); rel = new MainEntityRelation(); rel.setEntity1(e3); rel.setEntity2(e3); + rel.setCat2(cat); DB.save(rel); } From 0d9b3208bbb6e8b495efc580b5f852f9c89a7f0b Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 15:51:32 +0200 Subject: [PATCH 15/36] NOPR - EntityExtension base code --- .../src/main/java/io/ebean/EbeanVersion.java | 4 +- .../main/java/io/ebean/bean/EntityBean.java | 22 + .../io/ebean/bean/EntityBeanIntercept.java | 20 + .../ebean/bean/EntityExtensionIntercept.java | 562 ++++++++++++++++++ .../java/io/ebean/bean/ExtensionAccessor.java | 34 ++ .../io/ebean/bean/ExtensionAccessors.java | 264 ++++++++ .../java/io/ebean/bean/InterceptBase.java | 116 ++++ .../java/io/ebean/bean/InterceptReadOnly.java | 20 +- .../io/ebean/bean/InterceptReadWrite.java | 73 ++- .../io/ebean/bean/extend/EntityExtension.java | 70 +++ .../extend/EntityExtensionSuperclass.java | 10 + .../io/ebean/bean/extend/ExtendableBean.java | 20 + ebean-api/src/main/java/module-info.java | 1 + .../test/java/io/ebean/EbeanVersionTest.java | 6 +- .../server/core/bootup/BootupClasses.java | 15 + .../server/deploy/BeanDescriptorManager.java | 11 +- .../meta/BeanPropertyElementSetter.java | 2 +- .../deploy/meta/DeployBeanDescriptor.java | 16 +- .../deploy/parse/DeployCreateProperties.java | 11 + .../properties/EnhanceBeanPropertyAccess.java | 8 +- ebean-test/pom.xml | 28 +- .../virtualprop/AbstractVirtualBase.java | 22 + .../tests/model/virtualprop/VirtualBase.java | 17 + .../tests/model/virtualprop/VirtualBaseA.java | 19 + .../tests/model/virtualprop/VirtualBaseB.java | 19 + .../model/virtualprop/VirtualBaseInherit.java | 23 + .../model/virtualprop/ext/Extension1.java | 31 + .../model/virtualprop/ext/Extension2.java | 32 + .../model/virtualprop/ext/Extension3.java | 45 ++ .../model/virtualprop/ext/Extension4.java | 26 + .../model/virtualprop/ext/Extension5.java | 23 + .../model/virtualprop/ext/Extension6.java | 31 + .../virtualprop/ext/TestVirtualProps.java | 191 ++++++ .../virtualprop/ext/VirtualAExtendOne.java | 44 ++ .../ext/VirtualExtendManyToMany.java | 43 ++ .../virtualprop/ext/VirtualExtendOne.java | 46 ++ pom.xml | 62 +- 37 files changed, 1916 insertions(+), 71 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java create mode 100644 ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java create mode 100644 ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java create mode 100644 ebean-api/src/main/java/io/ebean/bean/InterceptBase.java create mode 100644 ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java create mode 100644 ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java create mode 100644 ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/AbstractVirtualBase.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBase.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseA.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseB.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseInherit.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension1.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension2.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension3.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension4.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension5.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension6.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/ext/TestVirtualProps.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualAExtendOne.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendManyToMany.java create mode 100644 ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendOne.java diff --git a/ebean-api/src/main/java/io/ebean/EbeanVersion.java b/ebean-api/src/main/java/io/ebean/EbeanVersion.java index aeb855efce..8c543e3fe4 100644 --- a/ebean-api/src/main/java/io/ebean/EbeanVersion.java +++ b/ebean-api/src/main/java/io/ebean/EbeanVersion.java @@ -22,8 +22,8 @@ public final class EbeanVersion { /** * Maintain the minimum ebean-agent version manually based on required ebean-agent bug fixes. */ - private static final int MIN_AGENT_MAJOR_VERSION = 12; - private static final int MIN_AGENT_MINOR_VERSION = 12; + private static final int MIN_AGENT_MAJOR_VERSION = 13; + private static final int MIN_AGENT_MINOR_VERSION = 10; private static String version = "unknown"; static { diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBean.java b/ebean-api/src/main/java/io/ebean/bean/EntityBean.java index 53bfc9ba49..1a70a68571 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBean.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBean.java @@ -41,6 +41,13 @@ default Object _ebean_newInstanceReadOnly() { throw new NotEnhancedException(); } + /** + * Creates a new instance and uses the provided intercept. (For EntityExtension) + */ + default Object _ebean_newExtendedInstance(int offset, EntityBean base) { + throw new NotEnhancedException(); + } + /** * Generated method that sets the loaded state on all the embedded beans on * this entity bean by using EntityBeanIntercept.setEmbeddedLoaded(Object o); @@ -120,4 +127,19 @@ default Object _ebean_getFieldIntercept(int fieldIndex) { default void toString(ToStringBuilder builder) { throw new NotEnhancedException(); } + + /** + * Returns the ExtensionAccessors, this is always NONE for non extendable beans. + */ + default ExtensionAccessors _ebean_getExtensionAccessors() { + return ExtensionAccessors.NONE; + } + + /** + * Returns the extension bean for an accessor. This will throw NotEnhancedException for non extendable beans. + * (It is not intended to call this method here) + */ + default EntityBean _ebean_getExtension(ExtensionAccessor accessor) { + throw new NotEnhancedException(); // not an extendableBean + } } diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java index 1127c02b25..120068ad34 100644 --- a/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java +++ b/ebean-api/src/main/java/io/ebean/bean/EntityBeanIntercept.java @@ -552,4 +552,24 @@ public interface EntityBeanIntercept extends Serializable { * Update the 'next' mutable info returning the content that was obtained via dirty detection. */ String mutableNext(int propertyIndex); + + /** + * Returns the value of the property. Can also return virtual properties. + */ + Object value(int propertyIndex); + + /** + * Returns the value of the property with intercept access. Can also return virtual properties. + */ + Object valueIntercept(int propertyIndex); + + /** + * Writes the value to the property. Can also write virtual properties. + */ + void setValue(int propertyIndex, Object value); + + /** + * Writes the value to the property with intercept access. Can also write virtual properties. + */ + void setValueIntercept(int propertyIndex, Object value); } diff --git a/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java b/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java new file mode 100644 index 0000000000..9efdd8ee07 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/EntityExtensionIntercept.java @@ -0,0 +1,562 @@ +package io.ebean.bean; + +import io.ebean.ValuePair; + +import java.util.Map; +import java.util.Set; + +/** + * Intercept for classes annotated with @EntityExtension. The intercept will delegate all calls to the base intercept of the + * ExtendableBean and adds given offset to all field operations. + * + * @author Roland Praml, FOCONIS AG + */ +public class EntityExtensionIntercept implements EntityBeanIntercept { + private final EntityBeanIntercept base; + private final int offset; + + public EntityExtensionIntercept(Object ownerBean, int offset, EntityBean base) { + this.base = base._ebean_getIntercept(); + this.offset = offset; + } + + @Override + public EntityBean owner() { + return base.owner(); + } + + @Override + public PersistenceContext persistenceContext() { + return base.persistenceContext(); + } + + @Override + public void setPersistenceContext(PersistenceContext persistenceContext) { + base.setPersistenceContext(persistenceContext); + } + + @Override + public void setNodeUsageCollector(NodeUsageCollector usageCollector) { + base.setNodeUsageCollector(usageCollector); + } + + @Override + public Object ownerId() { + return base.ownerId(); + } + + @Override + public void setOwnerId(Object ownerId) { + base.setOwnerId(ownerId); + } + + @Override + public Object embeddedOwner() { + return base.embeddedOwner(); + } + + @Override + public int embeddedOwnerIndex() { + return base.embeddedOwnerIndex(); + } + + @Override + public void clearGetterCallback() { + base.clearGetterCallback(); + } + + @Override + public void registerGetterCallback(PreGetterCallback getterCallback) { + base.registerGetterCallback(getterCallback); + } + + @Override + public void setEmbeddedOwner(EntityBean parentBean, int embeddedOwnerIndex) { + base.setEmbeddedOwner(parentBean, embeddedOwnerIndex); + } + + @Override + public void setBeanLoader(BeanLoader beanLoader, PersistenceContext ctx) { + base.setBeanLoader(beanLoader, ctx); + } + + @Override + public void setBeanLoader(BeanLoader beanLoader) { + base.setBeanLoader(beanLoader); + } + + @Override + public boolean isFullyLoadedBean() { + return base.isFullyLoadedBean(); + } + + @Override + public void setFullyLoadedBean(boolean fullyLoadedBean) { + base.setFullyLoadedBean(fullyLoadedBean); + } + + @Override + public boolean isPartial() { + return base.isPartial(); + } + + @Override + public boolean isDirty() { + return base.isDirty(); + } + + @Override + public void setEmbeddedDirty(int embeddedProperty) { + base.setEmbeddedDirty(embeddedProperty + offset); + } + + @Override + public void setDirty(boolean dirty) { + base.setDirty(dirty); + } + + @Override + public boolean isNew() { + return base.isNew(); + } + + @Override + public boolean isNewOrDirty() { + return base.isNewOrDirty(); + } + + @Override + public boolean hasIdOnly(int idIndex) { + return base.hasIdOnly(idIndex + offset); + } + + @Override + public boolean isReference() { + return base.isReference(); + } + + @Override + public void setReference(int idPos) { + base.setReference(idPos + offset); + } + + @Override + public void setLoadedFromCache(boolean loadedFromCache) { + base.setLoadedFromCache(loadedFromCache); + } + + @Override + public boolean isLoadedFromCache() { + return base.isLoadedFromCache(); + } + + @Override + public boolean isReadOnly() { + return base.isReadOnly(); + } + + @Override + public void setReadOnly(boolean readOnly) { + base.setReadOnly(readOnly); + } + + @Override + public void setForceUpdate(boolean forceUpdate) { + base.setForceUpdate(forceUpdate); + } + + @Override + public boolean isUpdate() { + return base.isUpdate(); + } + + @Override + public boolean isLoaded() { + return base.isLoaded(); + } + + @Override + public void setNew() { + base.setNew(); + } + + @Override + public void setLoaded() { + base.setLoaded(); + } + + @Override + public void setLoadedLazy() { + base.setLoadedLazy(); + } + + @Override + public void setLazyLoadFailure(Object ownerId) { + base.setLazyLoadFailure(ownerId); + } + + @Override + public boolean isLazyLoadFailure() { + return base.isLazyLoadFailure(); + } + + @Override + public boolean isDisableLazyLoad() { + return base.isDisableLazyLoad(); + } + + @Override + public void setDisableLazyLoad(boolean disableLazyLoad) { + base.setDisableLazyLoad(disableLazyLoad); + } + + @Override + public void setEmbeddedLoaded(Object embeddedBean) { + base.setEmbeddedLoaded(embeddedBean); + } + + @Override + public boolean isEmbeddedNewOrDirty(Object embeddedBean) { + return base.isEmbeddedNewOrDirty(embeddedBean); + } + + @Override + public Object origValue(int propertyIndex) { + return base.origValue(propertyIndex + offset); + } + + @Override + public int findProperty(String propertyName) { + return base.findProperty(propertyName); + } + + @Override + public String property(int propertyIndex) { + return base.property(propertyIndex + offset); + } + + @Override + public int propertyLength() { + return base.propertyLength(); + } + + @Override + public void setPropertyLoaded(String propertyName, boolean loaded) { + base.setPropertyLoaded(propertyName, loaded); + } + + @Override + public void setPropertyUnloaded(int propertyIndex) { + base.setPropertyUnloaded(propertyIndex + offset); + } + + @Override + public void setLoadedProperty(int propertyIndex) { + base.setLoadedProperty(propertyIndex + offset); + } + + @Override + public void setLoadedPropertyAll() { + base.setLoadedPropertyAll(); + } + + @Override + public boolean isLoadedProperty(int propertyIndex) { + return base.isLoadedProperty(propertyIndex + offset); + } + + @Override + public boolean isChangedProperty(int propertyIndex) { + return base.isChangedProperty(propertyIndex + offset); + } + + @Override + public boolean isDirtyProperty(int propertyIndex) { + return base.isDirtyProperty(propertyIndex + offset); + } + + @Override + public void markPropertyAsChanged(int propertyIndex) { + base.markPropertyAsChanged(propertyIndex + offset); + } + + @Override + public void setChangedProperty(int propertyIndex) { + base.setChangedProperty(propertyIndex + offset); + } + + @Override + public void setChangeLoaded(int propertyIndex) { + base.setChangeLoaded(propertyIndex + offset); + } + + @Override + public void setEmbeddedPropertyDirty(int propertyIndex) { + base.setEmbeddedPropertyDirty(propertyIndex + offset); + } + + @Override + public void setOriginalValue(int propertyIndex, Object value) { + base.setOriginalValue(propertyIndex + offset, value); + } + + @Override + public void setOriginalValueForce(int propertyIndex, Object value) { + base.setOriginalValueForce(propertyIndex + offset, value); + } + + @Override + public void setNewBeanForUpdate() { + base.setNewBeanForUpdate(); + } + + @Override + public Set loadedPropertyNames() { + return base.loadedPropertyNames(); + } + + @Override + public boolean[] dirtyProperties() { + return base.dirtyProperties(); + } + + @Override + public Set dirtyPropertyNames() { + return base.dirtyPropertyNames(); + } + + @Override + public void addDirtyPropertyNames(Set props, String prefix) { + base.addDirtyPropertyNames(props, prefix); + } + + @Override + public boolean hasDirtyProperty(Set propertyNames) { + return base.hasDirtyProperty(propertyNames); + } + + @Override + public Map dirtyValues() { + return base.dirtyValues(); + } + + @Override + public void addDirtyPropertyValues(Map dirtyValues, String prefix) { + base.addDirtyPropertyValues(dirtyValues, prefix); + } + + @Override + public void addDirtyPropertyValues(BeanDiffVisitor visitor) { + base.addDirtyPropertyValues(visitor); + } + + @Override + public StringBuilder dirtyPropertyKey() { + return base.dirtyPropertyKey(); + } + + @Override + public void addDirtyPropertyKey(StringBuilder sb) { + base.addDirtyPropertyKey(sb); + } + + @Override + public StringBuilder loadedPropertyKey() { + return base.loadedPropertyKey(); + } + + @Override + public boolean[] loaded() { + return base.loaded(); + } + + @Override + public int lazyLoadPropertyIndex() { + return base.lazyLoadPropertyIndex() - offset; + } + + @Override + public String lazyLoadProperty() { + return base.lazyLoadProperty(); + } + + @Override + public void loadBean(int loadProperty) { + base.loadBean(loadProperty); + } + + @Override + public void loadBeanInternal(int loadProperty, BeanLoader loader) { + base.loadBeanInternal(loadProperty + offset, loader); + } + + @Override + public void initialisedMany(int propertyIndex) { + base.initialisedMany(propertyIndex + offset); + } + + @Override + public void preGetterCallback(int propertyIndex) { + base.preGetterCallback(propertyIndex + offset); + } + + @Override + public void preGetId() { + base.preGetId(); + } + + @Override + public void preGetter(int propertyIndex) { + base.preGetter(propertyIndex + offset); + } + + @Override + public void preSetterMany(boolean interceptField, int propertyIndex, Object oldValue, Object newValue) { + base.preSetterMany(interceptField, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void setChangedPropertyValue(int propertyIndex, boolean setDirtyState, Object origValue) { + base.setChangedPropertyValue(propertyIndex + offset, setDirtyState, origValue); + } + + @Override + public void setDirtyStatus() { + base.setDirtyStatus(); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, Object oldValue, Object newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, boolean oldValue, boolean newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, int oldValue, int newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, long oldValue, long newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, double oldValue, double newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, float oldValue, float newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, short oldValue, short newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, char oldValue, char newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, byte oldValue, byte newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, char[] oldValue, char[] newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void preSetter(boolean intercept, int propertyIndex, byte[] oldValue, byte[] newValue) { + base.preSetter(intercept, propertyIndex + offset, oldValue, newValue); + } + + @Override + public void setOldValue(int propertyIndex, Object oldValue) { + base.setOldValue(propertyIndex + offset, oldValue); + } + + @Override + public int sortOrder() { + return base.sortOrder(); + } + + @Override + public void setSortOrder(int sortOrder) { + base.setSortOrder(sortOrder); + } + + @Override + public void setDeletedFromCollection(boolean deletedFromCollection) { + base.setDeletedFromCollection(deletedFromCollection); + } + + @Override + public boolean isOrphanDelete() { + return base.isOrphanDelete(); + } + + @Override + public void setLoadError(int propertyIndex, Exception t) { + base.setLoadError(propertyIndex + offset, t); + } + + @Override + public Map loadErrors() { + return base.loadErrors(); + } + + @Override + public boolean isChangedProp(int propertyIndex) { + return base.isChangedProp(propertyIndex + offset); + } + + @Override + public MutableValueInfo mutableInfo(int propertyIndex) { + return base.mutableInfo(propertyIndex + offset); + } + + @Override + public void mutableInfo(int propertyIndex, MutableValueInfo info) { + base.mutableInfo(propertyIndex + offset, info); + } + + @Override + public void mutableNext(int propertyIndex, MutableValueNext next) { + base.mutableNext(propertyIndex + offset, next); + } + + @Override + public String mutableNext(int propertyIndex) { + return base.mutableNext(propertyIndex + offset); + } + + @Override + public Object value(int propertyIndex) { + return base.value(propertyIndex + offset); + } + + @Override + public Object valueIntercept(int propertyIndex) { + return base.valueIntercept(propertyIndex + offset); + } + + @Override + public void setValue(int propertyIndex, Object value) { + base.setValue(propertyIndex + offset, value); + } + + @Override + public void setValueIntercept(int propertyIndex, Object value) { + base.setValueIntercept(propertyIndex + offset, value); + } +} diff --git a/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java new file mode 100644 index 0000000000..cfea5db354 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessor.java @@ -0,0 +1,34 @@ +package io.ebean.bean; + +import io.ebean.bean.extend.ExtendableBean; + +/** + * Provides access to the EntityExtensions. Each ExtendableBean may have multiple Extension-Accessors stored in the static + * {@link ExtensionAccessors} per class level. + *

+ * This interface is internally used by the enhancer. + * + * @author Roland Praml, FOCONIS AG + */ +public interface ExtensionAccessor { + + /* + * Returns the extension for a given bean. + */ + EntityBean getExtension(ExtendableBean bean); + + /** + * Returns the index of this extension. + */ + int getIndex(); + + /** + * Return the type of this extension. + */ + Class getType(); + + /** + * Returns the additional properties of this extension. + */ + String[] getProperties(); +} diff --git a/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java new file mode 100644 index 0000000000..341f3b4c31 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/ExtensionAccessors.java @@ -0,0 +1,264 @@ +package io.ebean.bean; + +import io.ebean.bean.extend.EntityExtension; +import io.ebean.bean.extend.ExtendableBean; + +import java.util.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Each ExtendableBean has one static member defined as + *

+ * private static ExtensionAccessors _ebean_extension_accessors =
+ *   new ExtensionAccessors(thisClass._ebeanProps, superClass._ebean_extension_accessors | null)
+ * 
+ * The ExtensionAccessors class is used to compute the additional space, that has to be reserved + * in the descriptor and the virtual properties, that will be added to the bean descriptor. + * The following class structure: + *
+ *   @Entity
+ *   class Base extends ExtendableBean {
+ *     String prop0;
+ *     String prop1;
+ *     String prop2;
+ *   }
+ *   @EntityExtends(Base.class)
+ *   class Ext1 {
+ *     String prop3;
+ *     String prop4;
+ *   }
+ *   @EntityExtends(Base.class)
+ *   class Ext2 {
+ *     String prop5;
+ *     String prop6;
+ *   }
+ * 
+ * will create an EntityBeanIntercept for "Base" holding up to 7 fields. Writing to fields 0..2 with ebi.setValue will modify + * the fields in Base, the r/w accesses to fields 3..4 are routed to Ext1 and 5..6 to Ext2. + *

+ * Note about offset and index: + *

+ *

+ * When you have subclasses (class Child extends Base) the extensions have all the same index in the parent and in + * the subclass, but may have different offsets, as the Child-class will provide additional fields. + *

+ * + * @author Roland Praml, FOCONIS AG + */ +public class ExtensionAccessors implements Iterable { + + /** + * Default extension info for beans, that have no extension. + */ + public static ExtensionAccessors NONE = new ExtensionAccessors(); + + /** + * The start offset specifies the offset where the first extension will start + */ + private final int startOffset; + + /** + * The entries. + */ + private List accessors = new ArrayList<>(); + + /** + * If we inherit from a class that has extensions, we have to inherit also all extensions from here + */ + private final ExtensionAccessors parent; + + /** + * The total property length of all extensions. This will be initialized once and cannot be changed any more + */ + private volatile int propertyLength = -1; + + /** + * The offsets where the extensions will start for effective binary search. + */ + private int[] offsets; + + /** + * Lock for synchronizing the initialization. + */ + private static final Lock lock = new ReentrantLock(); + + /** + * Constructor for ExtensionInfo.NONE. + */ + private ExtensionAccessors() { + this.startOffset = Integer.MAX_VALUE; + this.propertyLength = 0; + this.parent = null; + } + + /** + * Called from enhancer. Each entity has a static field initialized with + * _ebean_extensions = new ExtensonInfo(thisClass._ebeanProps, superClass._ebean_extensions | null) + */ + public ExtensionAccessors(String[] props, ExtensionAccessors parent) { + this.startOffset = props.length; + this.parent = parent; + } + + /** + * Called from enhancer. Each class annotated with {@link EntityExtension} will show up here. + * + * @param prototype instance of the class that is annotated with {@link EntityExtension} + */ + public ExtensionAccessor add(EntityBean prototype) { + if (propertyLength != -1) { + throw new UnsupportedOperationException("The extension is already in use and cannot be extended anymore"); + } + Entry entry = new Entry(prototype); + lock.lock(); + try { + accessors.add(entry); + } finally { + lock.unlock(); + } + return entry; + } + + /** + * returns how many extensions are registered. + */ + public int size() { + init(); + return accessors.size(); + } + + /** + * Returns the additional properties, that have been added by extensions. + */ + public int getPropertyLength() { + init(); + return propertyLength; + } + + /** + * Copies parent extensions and initializes the offsets. This will be done once only. + */ + private void init() { + if (propertyLength != -1) { + return; + } + lock.lock(); + try { + if (propertyLength != -1) { + return; + } + // sort the accessors, so they are "stable" + if (parent != null) { + parent.init(); + accessors.sort(Comparator.comparing(e -> e.getType().getName())); + if (accessors.isEmpty()) { + accessors = parent.accessors; + } else { + accessors.addAll(0, parent.accessors); + } + } + int length = 0; + offsets = new int[accessors.size()]; + for (int i = 0; i < accessors.size(); i++) { + Entry entry = (Entry) accessors.get(i); + entry.index = i; + offsets[i] = startOffset + length; + length += entry.getProperties().length; + } + propertyLength = length; + } finally { + lock.unlock(); + } + } + + /** + * Returns the offset of this extension accessor. + * Note: The offset may vary on subclasses + */ + int getOffset(ExtensionAccessor accessor) { + return offsets[accessor.getIndex()]; + } + + /** + * Finds the accessor for a given property. If the propertyIndex is lower than startOffset, no accessor will be returned, + * as this means that we try to access a property in the base-entity. + */ + ExtensionAccessor findAccessor(int propertyIndex) { + init(); + if (propertyIndex < startOffset) { + return null; + } + int pos = Arrays.binarySearch(offsets, propertyIndex); + if (pos == -1) { + return null; + } + if (pos < 0) { + pos = -2 - pos; + } + return accessors.get(pos); + } + + @Override + public Iterator iterator() { + init(); + return accessors.iterator(); + } + + /** + * Invoked by enhancer. + */ + public EntityBean createInstance(ExtensionAccessor accessor, EntityBean base) { + int offset = getOffset(accessor); + return ((Entry) accessor).createInstance(offset, base); + } + + static class Entry implements ExtensionAccessor { + private int index; + private final EntityBean prototype; + + private Entry(EntityBean prototype) { + this.prototype = prototype; + } + + @Override + public String[] getProperties() { + return prototype._ebean_getPropertyNames(); + } + + @Override + public int getIndex() { + return index; + } + + @Override + public Class getType() { + return prototype.getClass(); + } + + EntityBean createInstance(int offset, EntityBean base) { + return (EntityBean) prototype._ebean_newExtendedInstance(offset, base); + } + + @Override + public EntityBean getExtension(ExtendableBean bean) { + EntityBean eb = (EntityBean) bean; + return eb._ebean_getExtension(Entry.this); + } + } + + /** + * Reads the extension accessors for a given class. If the provided type is not an ExtenadableBean, the + * ExtensionAccessors.NONE is returned. + */ + public static ExtensionAccessors read(Class type) { + if (ExtendableBean.class.isAssignableFrom(type)) { + try { + return (ExtensionAccessors) type.getField("_ebean_extension_accessors").get(null); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Could not read extension info from " + type, e); + } + } + return ExtensionAccessors.NONE; + } +} diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java new file mode 100644 index 0000000000..a38655100b --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptBase.java @@ -0,0 +1,116 @@ +package io.ebean.bean; + +/** + * Base class for InterceptReadOnly / InterceptReadWrite. This class should contain only the essential member variables to keep + * the memory footprint low. + * + * @author Roland Praml, FOCONIS AG + */ +public abstract class InterceptBase implements EntityBeanIntercept { + + /** + * The actual entity bean that 'owns' this intercept. + */ + protected final EntityBean owner; + + protected InterceptBase(EntityBean owner) { + this.owner = owner; + } + + protected ExtensionAccessor findAccessor(int index) { + return owner._ebean_getExtensionAccessors().findAccessor(index); + } + + private int getOffset(ExtensionAccessor accessor) { + return owner._ebean_getExtensionAccessors().getOffset(accessor); + } + + protected EntityBean getExtensionBean(ExtensionAccessor accessor) { + return owner._ebean_getExtension(accessor); + } + + @Override + public int findProperty(String propertyName) { + String[] names = owner._ebean_getPropertyNames(); + int i; + for (i = 0; i < names.length; i++) { + if (names[i].equals(propertyName)) { + return i; + } + } + for (ExtensionAccessor acc : owner._ebean_getExtensionAccessors()) { + names = acc.getProperties(); + for (int j = 0; j < names.length; j++) { + if (names[j].equals(propertyName)) { + return i; + } + i++; + } + } + return -1; + } + + @Override + public String property(int propertyIndex) { + if (propertyIndex == -1) { + return null; + } + ExtensionAccessor accessor = findAccessor(propertyIndex); + if (accessor == null) { + return owner._ebean_getPropertyName(propertyIndex); + } else { + int offset = getOffset(accessor); + return getExtensionBean(accessor)._ebean_getPropertyName(propertyIndex - offset); + } + } + + @Override + public int propertyLength() { + return owner._ebean_getPropertyNames().length + + owner._ebean_getExtensionAccessors().getPropertyLength(); + } + + @Override + public Object value(int index) { + ExtensionAccessor accessor = findAccessor(index); + if (accessor == null) { + return owner._ebean_getField(index); + } else { + int offset = getOffset(accessor); + return getExtensionBean(accessor)._ebean_getField(index - offset); + } + } + + @Override + public Object valueIntercept(int index) { + ExtensionAccessor accessor = findAccessor(index); + if (accessor == null) { + return owner._ebean_getFieldIntercept(index); + } else { + int offset = getOffset(accessor); + return getExtensionBean(accessor)._ebean_getFieldIntercept(index - offset); + } + } + + @Override + public void setValue(int index, Object value) { + ExtensionAccessor accessor = findAccessor(index); + if (accessor == null) { + owner._ebean_setField(index, value); + } else { + int offset = getOffset(accessor); + getExtensionBean(accessor)._ebean_setField(index - offset, value); + } + } + + @Override + public void setValueIntercept(int index, Object value) { + ExtensionAccessor accessor = findAccessor(index); + if (accessor == null) { + owner._ebean_setFieldIntercept(index, value); + } else { + int offset = getOffset(accessor); + getExtensionBean(accessor)._ebean_setFieldIntercept(index - offset, value); + } + } +} diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java index 26f5faf8d5..da37254e2c 100644 --- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadOnly.java @@ -13,15 +13,14 @@ * required for updates such as per property changed, loaded, dirty state, original values * bean state etc. */ -public class InterceptReadOnly implements EntityBeanIntercept { +public class InterceptReadOnly extends InterceptBase implements EntityBeanIntercept { - private final EntityBean owner; /** * Create with a given entity. */ public InterceptReadOnly(Object ownerBean) { - this.owner = (EntityBean) ownerBean; + super((EntityBean) ownerBean); } @Override @@ -234,21 +233,6 @@ public Object origValue(int propertyIndex) { return null; } - @Override - public int findProperty(String propertyName) { - return 0; - } - - @Override - public String property(int propertyIndex) { - return null; - } - - @Override - public int propertyLength() { - return 0; - } - @Override public void setPropertyLoaded(String propertyName, boolean loaded) { diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java index 3649e70298..72d9b9bd9f 100644 --- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java @@ -12,7 +12,12 @@ import java.io.InputStream; import java.math.BigDecimal; import java.net.URL; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -22,9 +27,8 @@ * This provides the mechanisms to support deferred fetching of reference beans * and oldValues generation for concurrency checking. */ -public final class InterceptReadWrite implements EntityBeanIntercept { - - private static final long serialVersionUID = -3664031775464862649L; +public final class InterceptReadWrite extends InterceptBase implements EntityBeanIntercept { + private static final long serialVersionUID = 1834735632647183821L; private static final int STATE_NEW = 0; private static final int STATE_REFERENCE = 1; @@ -55,11 +59,6 @@ public final class InterceptReadWrite implements EntityBeanIntercept { private String ebeanServerName; private boolean deletedFromCollection; - - /** - * The actual entity bean that 'owns' this intercept. - */ - private final EntityBean owner; private EntityBean embeddedOwner; private int embeddedOwnerIndex; /** @@ -101,15 +100,15 @@ public final class InterceptReadWrite implements EntityBeanIntercept { * Create with a given entity. */ public InterceptReadWrite(Object ownerBean) { - this.owner = (EntityBean) ownerBean; - this.flags = new byte[owner._ebean_getPropertyNames().length]; + super((EntityBean) ownerBean); + this.flags = new byte[super.propertyLength()]; } /** * EXPERIMENTAL - Constructor only for use by serialization frameworks. */ public InterceptReadWrite() { - this.owner = null; + super(null); this.flags = null; } @@ -227,7 +226,7 @@ public boolean isDirty() { } if (mutableInfo != null) { for (int i = 0; i < mutableInfo.length; i++) { - if (mutableInfo[i] != null && !mutableInfo[i].isEqualToObject(owner._ebean_getField(i))) { + if (mutableInfo[i] != null && !mutableInfo[i].isEqualToObject(value(i))) { dirty = true; break; } @@ -411,25 +410,6 @@ public Object origValue(int propertyIndex) { return origValues[propertyIndex]; } - @Override - public int findProperty(String propertyName) { - final String[] names = owner._ebean_getPropertyNames(); - for (int i = 0; i < names.length; i++) { - if (names[i].equals(propertyName)) { - return i; - } - } - return -1; - } - - @Override - public String property(int propertyIndex) { - if (propertyIndex == -1) { - return null; - } - return owner._ebean_getPropertyName(propertyIndex); - } - @Override public int propertyLength() { return flags.length; @@ -571,7 +551,7 @@ public void addDirtyPropertyNames(Set props, String prefix) { props.add((prefix == null ? property(i) : prefix + property(i))); } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { // an embedded property has been changed - recurse - final EntityBean embeddedBean = (EntityBean) owner._ebean_getField(i); + final EntityBean embeddedBean = (EntityBean) value(i); embeddedBean._ebean_getIntercept().addDirtyPropertyNames(props, property(i) + "."); } } @@ -579,9 +559,10 @@ public void addDirtyPropertyNames(Set props, String prefix) { @Override public boolean hasDirtyProperty(Set propertyNames) { - final String[] names = owner._ebean_getPropertyNames(); + String[] names = owner._ebean_getPropertyNames(); final int len = propertyLength(); - for (int i = 0; i < len; i++) { + int i; + for (i = 0; i < len; i++) { if (isChangedProp(i)) { if (propertyNames.contains(names[i])) { return true; @@ -592,6 +573,22 @@ public boolean hasDirtyProperty(Set propertyNames) { } } } + for (ExtensionAccessor acc : owner._ebean_getExtensionAccessors()) { + names = acc.getProperties(); + for (int j = 0; j < names.length; j++) { + if (isChangedProp(i)) { + if (propertyNames.contains(names[j])) { + return true; + } + } else if ((flags[i] & FLAG_EMBEDDED_DIRTY) != 0) { + if (propertyNames.contains(names[j])) { + return true; + } + } + i++; + } + } + return false; } @@ -609,7 +606,7 @@ public void addDirtyPropertyValues(Map dirtyValues, String pr if (isChangedProp(i)) { // the property has been changed on this bean final String propName = (prefix == null ? property(i) : prefix + property(i)); - final Object newVal = owner._ebean_getField(i); + final Object newVal = value(i); final Object oldVal = origValue(i); if (notEqual(oldVal, newVal)) { dirtyValues.put(propName, new ValuePair(newVal, oldVal)); @@ -628,7 +625,7 @@ public void addDirtyPropertyValues(BeanDiffVisitor visitor) { for (int i = 0; i < len; i++) { if (isChangedProp(i)) { // the property has been changed on this bean - final Object newVal = owner._ebean_getField(i); + final Object newVal = value(i); final Object oldVal = origValue(i); if (notEqual(oldVal, newVal)) { visitor.visit(i, newVal, oldVal); @@ -1036,7 +1033,7 @@ public Map loadErrors() { public boolean isChangedProp(int i) { if ((flags[i] & FLAG_CHANGED_PROP) != 0) { return true; - } else if (mutableInfo == null || mutableInfo[i] == null || mutableInfo[i].isEqualToObject(owner._ebean_getField(i))) { + } else if (mutableInfo == null || mutableInfo[i] == null || mutableInfo[i].isEqualToObject(value(i))) { return false; } else { // mark for change diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java new file mode 100644 index 0000000000..c62d099a4e --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtension.java @@ -0,0 +1,70 @@ +package io.ebean.bean.extend; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Annotation for a class to extend an existing class. + *

+ * Normally, you would have annotated like the following example + * + *

+ *   package basePkg;
+ *   import extPkg.MyExtEntity;
+ *   class MyBaseEntity {
+ *     // this line is mandatory, to allow deletion of MyBaseEntity
+ *     @OneToOne(optional = true, cascade = Cascade.ALL)
+ *     private MyExtEntity
+ *   }
+ *
+ *   package extPkg;
+ *   import basePkg.myBaseEntity;
+ *   class MyExtEntity {
+ *     @OneToOne(optional = false)
+ *     private MyBaseEntity
+ *   }
+ * 
+ *

+ * If you spread your code over different packages or (especially in different maven modules), you'll get problems, because you'll get cyclic depencencies. + *

+ * To break up these dependencies, you can annotate 'MyExtEntity' + * + *

+ *   package extPkg;
+ *   import basePkg.myBaseEntity;
+ *   @EntityExtension(MyBaseEntity.class)
+ *   class MyExtEntity {
+ *     @OneToOne(optional = false)
+ *     private MyBaseEntity
+ *
+ *     private String someField;
+ *   }
+ * 
+ * This will create a virtual property in the MyBaseEntity without adding a dependency to MyExtEntity. + *

+ * You may add a + *

+ *   public static MyExtEntity get(MyBaseEntity base) {
+ *     throw new NotEnhancedException();
+ *   }
+ * 
+ * This getter will be replaced by the enhancer, so that you can easily get it with + * MyExtEntiy.get(base).getSomeField(). + *
+ * Technically, the instance of MyExtEntiy is stored in the _ebean_extension_storage array of + * MyBaseEntity. + *

+ * If you save the MyBaseEntity, it will also save the data stored in MyExtEntity. + * + * @author Alexander Wagner, FOCONIS AG + */ +@Documented +@Target(TYPE) +@Retention(RUNTIME) +public @interface EntityExtension { + Class[] value(); +} diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java new file mode 100644 index 0000000000..cb491c3508 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/extend/EntityExtensionSuperclass.java @@ -0,0 +1,10 @@ +package io.ebean.bean.extend; + +/** + * Marker for EntityExtension superclass. + * + * @author Noemi Praml, FOCONIS AG + */ +public @interface EntityExtensionSuperclass { + +} diff --git a/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java b/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java new file mode 100644 index 0000000000..1f4f4b7de1 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/bean/extend/ExtendableBean.java @@ -0,0 +1,20 @@ +package io.ebean.bean.extend; + +import io.ebean.bean.EntityBean; +import io.ebean.bean.NotEnhancedException; + +/** + * Marker interface for beans that can be extended with @EntityExtension. + * + * @author Roland Praml, FOCONIS AG + */ +public interface ExtendableBean { + + /** + * Returns an array of registered extensions. This may be useful for bean validation. + * NOTE: The passed array should NOT be modified. + */ + default EntityBean[] _ebean_getExtensions() { + throw new NotEnhancedException(); + } +} diff --git a/ebean-api/src/main/java/module-info.java b/ebean-api/src/main/java/module-info.java index 27548d1ee6..3d6f4da50d 100644 --- a/ebean-api/src/main/java/module-info.java +++ b/ebean-api/src/main/java/module-info.java @@ -21,6 +21,7 @@ exports io.ebean; exports io.ebean.bean; + exports io.ebean.bean.extend; exports io.ebean.cache; exports io.ebean.meta; exports io.ebean.config; diff --git a/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java b/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java index 5eb17bbe87..5b78dbbe15 100644 --- a/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java +++ b/ebean-api/src/test/java/io/ebean/EbeanVersionTest.java @@ -9,9 +9,9 @@ class EbeanVersionTest { @Test void checkMinAgentVersion_ok() { - assertFalse(EbeanVersion.checkMinAgentVersion("12.12.0")); - assertFalse(EbeanVersion.checkMinAgentVersion("12.12.99")); - assertFalse(EbeanVersion.checkMinAgentVersion("13.1.0")); + assertFalse(EbeanVersion.checkMinAgentVersion("13.10.0")); + assertFalse(EbeanVersion.checkMinAgentVersion("13.10.99")); + assertFalse(EbeanVersion.checkMinAgentVersion("14.1.0")); } @Test diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java index 57b6afd04a..4b89778d0d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/bootup/BootupClasses.java @@ -2,6 +2,7 @@ import io.ebean.annotation.DocStore; import io.ebean.DatabaseBuilder; +import io.ebean.bean.extend.EntityExtension; import io.ebean.config.IdGenerator; import io.ebean.config.ScalarTypeConverter; import io.ebean.core.type.ScalarType; @@ -39,6 +40,7 @@ public class BootupClasses implements Predicate> { private final List> embeddableList = new ArrayList<>(); private final List> entityList = new ArrayList<>(); + private final List> entityExtensionList = new ArrayList<>(); private final List>> scalarTypeList = new ArrayList<>(); private final List>> scalarConverterList = new ArrayList<>(); private final List>> attributeConverterList = new ArrayList<>(); @@ -313,6 +315,13 @@ public List> getEntities() { return entityList; } + /** + * Return the list of entity extension classes. + */ + public List> getEntityExtensionList() { + return entityExtensionList; + } + /** * Return the list of ScalarTypes found. */ @@ -340,6 +349,8 @@ public boolean test(Class cls) { embeddableList.add(cls); } else if (isEntity(cls)) { entityList.add(cls); + } else if (isEntityExtension(cls)) { + entityExtensionList.add(cls); } else { return isInterestingInterface(cls); } @@ -456,6 +467,10 @@ private boolean isEntity(Class cls) { return has(cls, Entity.class) || has(cls, Table.class) || has(cls, DocStore.class); } + private boolean isEntityExtension(Class cls) { + return has(cls, EntityExtension.class); + } + private boolean isEmbeddable(Class cls) { return has(cls, Embeddable.class); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index c14932c9a8..b9e16e3f69 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -654,6 +654,14 @@ private void registerDescriptor(DeployBeanInfo info) { * BeanTables have all been created. */ private void readEntityDeploymentInitial() { + for (Class extensionClass : bootupClasses.getEntityExtensionList()) { + try { + // TODO: load class in an early stage + extensionClass.getField("_ebean_props").get(null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } for (Class entityClass : bootupClasses.getEntities()) { DeployBeanInfo info = createDeployBeanInfo(entityClass); deployInfoMap.put(entityClass, info); @@ -1178,11 +1186,12 @@ private DeployBeanInfo createDeployBeanInfo(Class beanClass) { // set bean controller, finder and listener setBeanControllerFinderListener(desc); deplyInherit.process(desc); - desc.checkInheritanceMapping(); createProperties.createProperties(desc); DeployBeanInfo info = new DeployBeanInfo<>(deployUtil, desc); readAnnotations.readInitial(info); + + desc.checkInheritanceMapping(); return info; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/BeanPropertyElementSetter.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/BeanPropertyElementSetter.java index c8a362b602..fcb34d0e07 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/BeanPropertyElementSetter.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/BeanPropertyElementSetter.java @@ -16,7 +16,7 @@ final class BeanPropertyElementSetter implements BeanPropertySetter { @Override public void set(EntityBean bean, Object value) { - bean._ebean_setField(pos, value); + bean._ebean_getIntercept().setValue(pos, value); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index fe5b6192c5..69f3dd628d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -5,6 +5,8 @@ import io.ebean.annotation.DocStore; import io.ebean.annotation.DocStoreMode; import io.ebean.annotation.Identity; +import io.ebean.bean.ExtensionAccessor; +import io.ebean.bean.ExtensionAccessors; import io.ebean.config.TableName; import io.ebean.config.dbplatform.IdType; import io.ebean.config.dbplatform.PlatformIdGenerator; @@ -146,12 +148,24 @@ public BindMaxLength bindMaxLength() { private String[] readPropertyNames() { try { Field field = beanType.getField("_ebean_props"); - return (String[]) field.get(null); + String[] props = (String[]) field.get(null); + + for (ExtensionAccessor extension : ExtensionAccessors.read(beanType)) { + props = concat(props, extension.getProperties()); + } + return props; } catch (Exception e) { throw new IllegalStateException("Error getting _ebean_props field on type " + beanType, e); } } + private String[] concat(String[] arr1, String[] arr2) { + String[] ret = new String[arr1.length + arr2.length]; + System.arraycopy(arr1, 0, ret, 0, arr1.length); + System.arraycopy(arr2, 0, ret, arr1.length, arr2.length); + return ret; + } + public void setPropertyNames(String[] properties) { this.properties = properties; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java index 4e887bbbc8..9f9f050695 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/parse/DeployCreateProperties.java @@ -2,6 +2,8 @@ import io.ebean.Model; import io.ebean.annotation.*; +import io.ebean.bean.ExtensionAccessor; +import io.ebean.bean.ExtensionAccessors; import io.ebean.core.type.ScalarType; import io.ebean.util.AnnotationUtil; import io.ebeaninternal.api.CoreLog; @@ -36,6 +38,15 @@ public DeployCreateProperties(TypeManager typeManager) { */ public void createProperties(DeployBeanDescriptor desc) { createProperties(desc, desc.getBeanType(), 0); + for (ExtensionAccessor info : ExtensionAccessors.read(desc.getBeanType())) { + createProperties(desc, info.getType(), 0); + for (DeployBeanProperty prop : desc.propertiesAll()) { + if (prop.getOwningType() == info.getType()) { + prop.setOwningType(desc.getBeanType()); + } + } + } + desc.sortProperties(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/properties/EnhanceBeanPropertyAccess.java b/ebean-core/src/main/java/io/ebeaninternal/server/properties/EnhanceBeanPropertyAccess.java index e7e48b6f03..ce42a69873 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/properties/EnhanceBeanPropertyAccess.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/properties/EnhanceBeanPropertyAccess.java @@ -54,12 +54,12 @@ private static final class Getter implements BeanPropertyGetter { @Override public Object get(EntityBean bean) { - return bean._ebean_getField(fieldIndex); + return bean._ebean_intercept().value(fieldIndex); } @Override public Object getIntercept(EntityBean bean) { - return bean._ebean_getFieldIntercept(fieldIndex); + return bean._ebean_intercept().valueIntercept(fieldIndex); } } @@ -73,12 +73,12 @@ private static final class Setter implements BeanPropertySetter { @Override public void set(EntityBean bean, Object value) { - bean._ebean_setField(fieldIndex, value); + bean._ebean_intercept().setValue(fieldIndex, value); } @Override public void setIntercept(EntityBean bean, Object value) { - bean._ebean_setFieldIntercept(fieldIndex, value); + bean._ebean_intercept().setValueIntercept(fieldIndex, value); } } diff --git a/ebean-test/pom.xml b/ebean-test/pom.xml index 3450656c52..fc68ec53a6 100644 --- a/ebean-test/pom.xml +++ b/ebean-test/pom.xml @@ -302,7 +302,7 @@ - + + + io.ebean + ebean-maven-plugin + ${ebean-maven-plugin.version} + + + test + process-test-classes + + debug=0 + + + testEnhance + + + + + + + io.ebean + ebean-agent + ${ebean-agent.version} + + - diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/AbstractVirtualBase.java b/ebean-test/src/test/java/org/tests/model/virtualprop/AbstractVirtualBase.java new file mode 100644 index 0000000000..dc53eaa28b --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/AbstractVirtualBase.java @@ -0,0 +1,22 @@ +package org.tests.model.virtualprop; + +import io.ebean.bean.extend.ExtendableBean; + +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +public class AbstractVirtualBase implements ExtendableBean { + + @Id + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBase.java b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBase.java new file mode 100644 index 0000000000..b78a73b707 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBase.java @@ -0,0 +1,17 @@ +package org.tests.model.virtualprop; + +import jakarta.persistence.Entity; + +@Entity +public class VirtualBase extends AbstractVirtualBase { + + private String data; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseA.java b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseA.java new file mode 100644 index 0000000000..ef3a62e6a1 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseA.java @@ -0,0 +1,19 @@ +package org.tests.model.virtualprop; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +@Entity +@DiscriminatorValue("A") +public class VirtualBaseA extends VirtualBaseInherit { + + private Integer num; + + public Integer getNum() { + return num; + } + + public void setNum(Integer num) { + this.num = num; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseB.java b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseB.java new file mode 100644 index 0000000000..8b4dc4da58 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseB.java @@ -0,0 +1,19 @@ +package org.tests.model.virtualprop; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +@Entity +@DiscriminatorValue("B") +public class VirtualBaseB extends VirtualBaseInherit { + + private String text; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseInherit.java b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseInherit.java new file mode 100644 index 0000000000..2256a406dc --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/VirtualBaseInherit.java @@ -0,0 +1,23 @@ +package org.tests.model.virtualprop; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Entity; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +@Entity +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "kind") +public class VirtualBaseInherit extends AbstractVirtualBase { + + private String data; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension1.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension1.java new file mode 100644 index 0000000000..fbf1f11685 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension1.java @@ -0,0 +1,31 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.AbstractVirtualBase; + +/** + * This class will add the field 'ext' to 'VirtualBaseA' by EntityExtension + */ +@EntityExtension(AbstractVirtualBase.class) +public class Extension1 { + + + public static String foo() { + return "foo"; + } + + private String ext; + + public String getExt() { + return ext; + } + + public void setExt(String ext) { + this.ext = ext; + } + + public static Extension1 get(AbstractVirtualBase base) { + throw new NotEnhancedException(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension2.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension2.java new file mode 100644 index 0000000000..c67fad4def --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension2.java @@ -0,0 +1,32 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.VirtualBase; + +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import java.util.List; + +/** + * This class will add the fields 'virtualExtendManyToManys' to 'AbstractVirtualBase' by EntityExtension + */ +@EntityExtension(VirtualBase.class) +public class Extension2 { + + public static String foo() { + return "foo"; + } + + @ManyToMany + @JoinTable(name = "kreuztabelle") + private List virtualExtendManyToManys; + + public List getVirtualExtendManyToManys() { + return virtualExtendManyToManys; + } + + public static Extension2 get(VirtualBase found) { + throw new NotEnhancedException(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension3.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension3.java new file mode 100644 index 0000000000..cb5f4ea751 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension3.java @@ -0,0 +1,45 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.annotation.Formula; +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.VirtualBase; + +import jakarta.persistence.OneToOne; + +/** + * This class will add the fields 'virtualExtendOne' and 'firstName' to 'VirtualBase' by EntityExtension + */ +@EntityExtension(VirtualBase.class) +public class Extension3 { + + public static String foo() { + return "foo"; + } + + @OneToOne(mappedBy = "base") + private VirtualExtendOne virtualExtendOne; + + @Formula(select = "concat('Your name is ', ${ta}.data)") + private String firstName; + + public static Extension3 get(VirtualBase found) { + throw new NotEnhancedException(); + } + + public VirtualExtendOne getVirtualExtendOne() { + return virtualExtendOne; + } + + public void setVirtualExtendOne(VirtualExtendOne virtualExtendOne) { + this.virtualExtendOne = virtualExtendOne; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension4.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension4.java new file mode 100644 index 0000000000..8b8ee66cbc --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension4.java @@ -0,0 +1,26 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.VirtualBaseA; + +/** + * This class will add the field 'ext' to 'VirtualBaseA' by EntityExtension + */ +@EntityExtension(VirtualBaseA.class) +public class Extension4 { + + private String extA; + + public String getExtA() { + return extA; + } + + public void setExtA(String extA) { + this.extA = extA; + } + + public static Extension4 get(VirtualBaseA base) { + throw new NotEnhancedException(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension5.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension5.java new file mode 100644 index 0000000000..04bc147dba --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension5.java @@ -0,0 +1,23 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.VirtualBaseA; + +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; + +/** + * This class will add the field 'ext' to 'VirtualBaseA' by EntityExtension + */ +@EntityExtension(VirtualBaseA.class) +public class Extension5 { + + @OneToMany + private List virtualExtends = new ArrayList<>(); + + public static Extension5 get(VirtualBaseA base) { + throw new NotEnhancedException(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension6.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension6.java new file mode 100644 index 0000000000..b96c49e50a --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/Extension6.java @@ -0,0 +1,31 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.annotation.DbJson; +import io.ebean.bean.NotEnhancedException; +import io.ebean.bean.extend.EntityExtension; +import org.tests.model.virtualprop.VirtualBaseA; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * This class will add the field 'nums' to 'VirtualBaseA' by EntityExtension + */ +@EntityExtension(VirtualBaseA.class) +public class Extension6 { + + @DbJson + private Set nums = new LinkedHashSet<>(); + + public Set getNums() { + return nums; + } + + public void setNums(Set nums) { + this.nums = nums; + } + + public static Extension6 get(VirtualBaseA base) { + throw new NotEnhancedException(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/TestVirtualProps.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/TestVirtualProps.java new file mode 100644 index 0000000000..550f307398 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/TestVirtualProps.java @@ -0,0 +1,191 @@ +package org.tests.model.virtualprop.ext; + +import io.ebean.DB; +import io.ebean.Database; +import io.ebean.bean.EntityBean; +import io.ebean.plugin.BeanType; +import io.ebean.test.LoggedSql; +import io.ebeaninternal.server.deploy.BeanProperty; +import org.junit.jupiter.api.Test; +import org.tests.model.virtualprop.AbstractVirtualBase; +import org.tests.model.virtualprop.VirtualBase; +import org.tests.model.virtualprop.VirtualBaseA; + +import java.lang.reflect.Field; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Demo, how to use virtual properties. Note: that there is + * + * @author Roland Praml, FOCONIS AG + */ +public class TestVirtualProps { + + // must be called BEFORE DB-Init. + static String e2 = Extension3.foo() + Extension2.foo() + Extension1.foo(); + static Database db = DB.getDefault(); + + /* + private static Database createDb() { + DatabaseConfig config = new DatabaseConfig(); + config.setName("h2"); + config.loadFromProperties(); + config.setDdlExtra(false); + config.setPackages(List.of("org.tests.model.virtualprop")); + return DatabaseFactory.create(config); + }*/ + + @Test + void testCreate() throws NoSuchFieldException, IllegalAccessException { + System.out.println(e2); + //DB.getDefault(); // Init database to start parser + VirtualBase base = new VirtualBase(); + base.setData("Foo"); + + db.save(base); + + Field f = AbstractVirtualBase.class.getDeclaredField("_ebean_extension_storage"); + f.setAccessible(true); + EntityBean[] extension_storage = (EntityBean[]) f.get(base); + assertThat(extension_storage[0]).isInstanceOf(Extension1.class); + assertThat(extension_storage[1]).isInstanceOf(Extension2.class); + assertThat(extension_storage[2]).isInstanceOf(Extension3.class); + + VirtualBase found = db.find(VirtualBase.class).where().isNull("virtualExtendOne").findOne(); + assertThat(found).isNotNull(); + + found = db.find(VirtualBase.class).where().isNotNull("virtualExtendOne").findOne(); + assertThat(found).isNull(); + + BeanType bt = db.pluginApi().beanType(VirtualBase.class); + BeanProperty prop = (BeanProperty) bt.property("virtualExtendOne"); + + + found = db.find(VirtualBase.class).where().isNull("virtualExtendOne").findOne(); + VirtualExtendOne ext = new VirtualExtendOne(); + ext.setData("bar"); + + prop.pathSet(found, ext); + db.save(found); + + Extension3 other = Extension3.get(found); + assertThat(other.getVirtualExtendOne().getData()).isEqualTo("bar"); + other.setFirstName("test"); + + Extension2 many = Extension2.get(found); + assertThat(many.getVirtualExtendManyToManys()).isEmpty(); + other.getVirtualExtendOne().setData("faz"); + db.save(found); + + found = db.find(VirtualBase.class).where().eq("virtualExtendOne.data", "faz").findOne(); + assertThat(found).isNotNull(); + + List attr = db.find(VirtualBase.class).fetch("virtualExtendOne", "data").findSingleAttributeList(); + assertThat(attr).containsExactly("faz"); + + attr = db.find(VirtualBase.class).select("firstName").findSingleAttributeList(); + assertThat(attr).containsExactly("Your name is Foo"); + VirtualExtendOne oneFound = (VirtualExtendOne) prop.pathGet(found); + assertThat(oneFound.getData()).isEqualTo("faz"); + + db.delete(oneFound); // cleanup + } + + @Test + void testCreateMany() { + + VirtualBase base1 = new VirtualBase(); + base1.setData("Foo"); + db.save(base1); + + VirtualBase base2 = new VirtualBase(); + base2.setData("Bar"); + db.save(base2); + + VirtualExtendManyToMany many1 = new VirtualExtendManyToMany(); + many1.setData("Alex"); + db.save(many1); + + VirtualExtendManyToMany many2 = new VirtualExtendManyToMany(); + many2.setData("Roland"); + db.save(many2); + + BeanType bt = db.pluginApi().beanType(VirtualBase.class); + BeanProperty prop = (BeanProperty) bt.property("virtualExtendManyToManys"); + List list = (List) prop.pathGet(base1); + assertThat(list).isEmpty(); + list.add(many1); + LoggedSql.start(); + db.save(base1); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("insert into kreuztabelle (virtual_base_id, virtual_extend_many_to_many_id) values (?, ?)"); + assertThat(sql.get(1)).contains("-- bind"); + assertThat(sql.get(2)).contains("-- executeBatch() size:1 sql:insert into kreuztabelle (virtual_base_id, virtual_extend_many_to_many_id) values (?, ?)"); + + many2.getBases().add(base1); + LoggedSql.start(); + db.save(many2); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(3); + + + LoggedSql.start(); + VirtualBase found = db.find(VirtualBase.class, base1.getId()); + list = (List) prop.pathGet(found); + assertThat(list).hasSize(2).containsExactlyInAnyOrder(many1, many2); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(2); +// assertThat(sql.get(0)).contains("select t0.id, t0.data, concat('Your name is ', t0.data), t1.id from virtual_base t0 left join virtual_extend_one t1 on t1.id = t0.id where t0.id = ?"); + assertThat(sql.get(1)).startsWith("select int_.virtual_base_id, t0.id, t0.data from virtual_extend_many_to_many t0 left join kreuztabelle int_ on int_.virtual_extend_many_to_many_id = t0.id where (int_.virtual_base_id) "); + DB.find(VirtualBase.class).delete(); + DB.find(VirtualExtendManyToMany.class).delete(); + } + + @Test + void testCreateDelete() { + + VirtualBase base = new VirtualBase(); + base.setData("Master"); + db.save(base); + + VirtualExtendOne extendOne = new VirtualExtendOne(); + extendOne.setBase(base); + extendOne.setData("Extended"); + db.save(extendOne); + + VirtualBase found = db.find(VirtualBase.class, base.getId()); + + LoggedSql.start(); + db.delete(found); + List sql = LoggedSql.stop(); + + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("delete from virtual_extend_one where id = ?"); // delete OneToOne - why 'id=?' and not 'id = ?' + assertThat(sql.get(1)).contains("delete from kreuztabelle where virtual_base_id = ?"); // intersection table + assertThat(sql.get(2)).contains("delete from virtual_base where id=?"); // delete entity itself + + } + + @Test + void testInheritance() { + + VirtualBaseA base = new VirtualBaseA(); + base.setData("Master"); + Extension1.get(base).setExt("ext"); + //db.save(base); + + Extension4.get(base).setExtA("extA"); + db.save(base); + + LoggedSql.start(); + VirtualBaseA found = db.find(VirtualBaseA.class).where().eq("extA", "extA").findOne(); + List sql = LoggedSql.stop(); + + assertThat(sql).hasSize(1); + assertThat(sql.get(0)).contains("select t0.kind, t0.id, t0.data, t0.num, t0.ext, t0.ext_a, t0.nums from virtual_base_inherit t0 where t0.kind = 'A' and t0.ext_a = ?"); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualAExtendOne.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualAExtendOne.java new file mode 100644 index 0000000000..64dd3c32a3 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualAExtendOne.java @@ -0,0 +1,44 @@ +package org.tests.model.virtualprop.ext; + +import org.tests.model.virtualprop.VirtualBaseA; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +@Entity +public class VirtualAExtendOne { + + @Id + private int id; + + private String data; + + @ManyToOne + private VirtualBaseA base; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public VirtualBaseA getBase() { + return base; + } + + public void setBase(VirtualBaseA base) { + this.base = base; + this.id = base == null ? 0 : base.getId(); + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendManyToMany.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendManyToMany.java new file mode 100644 index 0000000000..5fe1e51096 --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendManyToMany.java @@ -0,0 +1,43 @@ +package org.tests.model.virtualprop.ext; + +import org.tests.model.virtualprop.VirtualBase; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import java.util.List; + +@Entity +public class VirtualExtendManyToMany { + @Id + private int id; + + private String data; + + @ManyToMany(mappedBy = "virtualExtendManyToManys") + private List bases; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public List getBases() { + return bases; + } + + public void setBases(List bases) { + this.bases = bases; + } +} diff --git a/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendOne.java b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendOne.java new file mode 100644 index 0000000000..73156dbe4b --- /dev/null +++ b/ebean-test/src/test/java/org/tests/model/virtualprop/ext/VirtualExtendOne.java @@ -0,0 +1,46 @@ +package org.tests.model.virtualprop.ext; + +import org.tests.model.virtualprop.VirtualBase; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.PrimaryKeyJoinColumn; + +@Entity +public class VirtualExtendOne { + + @Id + private int id; + + private String data; + + @PrimaryKeyJoinColumn + @OneToOne(optional = false) + private VirtualBase base; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public VirtualBase getBase() { + return base; + } + + public void setBase(VirtualBase base) { + this.base = base; + this.id = base == null ? 0 : base.getId(); + } +} diff --git a/pom.xml b/pom.xml index 2e07fb472d..e6f6ead660 100644 --- a/pom.xml +++ b/pom.xml @@ -53,7 +53,7 @@ 14.2.0 7.4 9.0 - 14.7.1 + 14.5.2-FOC1 14.7.1 false @@ -103,6 +103,66 @@ + + foconis + + true + + + + foconis-release + FOCONIS Release Repository + https://mvnrepo.foconis.de/repository/release/ + + + foconis-snapshot + FOCONIS Snapshot Repository + https://mvnrepo.foconis.de/repository/snapshot/ + + + + + foconis-release + https://mvnrepo.foconis.de/repository/release/ + + + foconis-snapshot + https://mvnrepo.foconis.de/repository/snapshot/ + + + + + foconis-release + https://mvnrepo.foconis.de/repository/release/ + + + foconis-snapshot + https://mvnrepo.foconis.de/repository/snapshot/ + + + + + github + + + github-release + FOCONIS Github Release Repository + https://maven.pkg.github.com/foconis/ebean + + + + + github-release + https://maven.pkg.github.com/foconis/ebean-agent + + + + + github-release + https://maven.pkg.github.com/foconis/ebean-agent + + + central From 971fdc57bcea418c6a83789f5d9ae095896657fe Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 16:13:44 +0200 Subject: [PATCH 16/36] NOPR - NEEDS Refactor: ADD: Support for custom metrics --- .../java/io/ebean/metric/CountMetric.java | 6 +--- .../src/main/java/io/ebean/metric/Metric.java | 13 +++++++ .../java/io/ebean/metric/MetricFactory.java | 13 +++++++ .../java/io/ebean/metric/MetricRegistry.java | 31 ++++++++++++++++ .../java/io/ebean/metric/QueryPlanMetric.java | 6 +--- .../java/io/ebean/metric/TimedMetric.java | 6 +--- .../java/io/ebean/metric/TimedMetricMap.java | 6 +--- .../server/cache/DefaultServerCache.java | 4 +++ .../server/core/DefaultServer.java | 5 +++ .../server/profile/AbstractMetric.java | 25 +++++++++++++ .../server/profile/DCountMetric.java | 21 ----------- .../server/profile/DCountMetricStats.java | 27 ++++++++++++++ .../server/profile/DIntMetric.java | 36 +++++++++++++++++++ .../server/profile/DLongMetric.java | 36 +++++++++++++++++++ .../server/profile/DMetricFactory.java | 14 ++++++++ 15 files changed, 208 insertions(+), 41 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/metric/Metric.java create mode 100644 ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/profile/AbstractMetric.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetricStats.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/profile/DIntMetric.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/profile/DLongMetric.java diff --git a/ebean-api/src/main/java/io/ebean/metric/CountMetric.java b/ebean-api/src/main/java/io/ebean/metric/CountMetric.java index 3ce785c1ea..d125134552 100644 --- a/ebean-api/src/main/java/io/ebean/metric/CountMetric.java +++ b/ebean-api/src/main/java/io/ebean/metric/CountMetric.java @@ -5,7 +5,7 @@ /** * Metric for timed events like transaction execution times. */ -public interface CountMetric { +public interface CountMetric extends Metric { /** * Add to the counter. @@ -32,8 +32,4 @@ public interface CountMetric { */ void reset(); - /** - * Visit non empty metrics. - */ - void visit(MetricVisitor visitor); } diff --git a/ebean-api/src/main/java/io/ebean/metric/Metric.java b/ebean-api/src/main/java/io/ebean/metric/Metric.java new file mode 100644 index 0000000000..374b2134e2 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/metric/Metric.java @@ -0,0 +1,13 @@ +package io.ebean.metric; + +import io.ebean.meta.MetricVisitor; + +/** + * Base for all metrics. + */ +public interface Metric { + /** + * Visit the underlying metric. + */ + void visit(MetricVisitor visitor); +} diff --git a/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java b/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java index b3dcc51606..b0d07ef2fb 100644 --- a/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java +++ b/ebean-api/src/main/java/io/ebean/metric/MetricFactory.java @@ -4,6 +4,9 @@ import io.ebean.XBootstrapService; import io.ebean.service.BootstrapService; +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; + /** * Factory to create timed metric counters. */ @@ -31,6 +34,16 @@ static MetricFactory get() { */ CountMetric createCountMetric(String name); + /** + * Create a metric, that gets the value from a supplier. + */ + Metric createMetric(String name, LongSupplier supplier); + + /** + * Create a metric, that gets the value from a supplier. + */ + Metric createMetric(String name, IntSupplier supplier); + /** * Create a Timed metric. */ diff --git a/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java b/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java new file mode 100644 index 0000000000..d881320247 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/metric/MetricRegistry.java @@ -0,0 +1,31 @@ +package io.ebean.metric; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Global registry of custom metrics instances created. + */ +public final class MetricRegistry { + public interface RegistryEntry { + void remove(); + } + + private static final List list = Collections.synchronizedList(new ArrayList<>()); + + /** + * Register the metric instance. + */ + public static RegistryEntry register(Metric location) { + list.add(location); + return () -> list.remove(location); + } + + /** + * Return all the registered extra metrics. + */ + public static List registered() { + return list; + } +} diff --git a/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java b/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java index 24999ce0ed..c1f24eea45 100644 --- a/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java +++ b/ebean-api/src/main/java/io/ebean/metric/QueryPlanMetric.java @@ -5,15 +5,11 @@ /** * Internal Query plan metric holder. */ -public interface QueryPlanMetric { +public interface QueryPlanMetric extends Metric { /** * Return the underlying timed metric. */ TimedMetric metric(); - /** - * Visit the underlying metric. - */ - void visit(MetricVisitor visitor); } diff --git a/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java b/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java index 17c3defb95..4ec95581a4 100644 --- a/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java +++ b/ebean-api/src/main/java/io/ebean/metric/TimedMetric.java @@ -5,7 +5,7 @@ /** * Metric for timed events like transaction execution times. */ -public interface TimedMetric { +public interface TimedMetric extends Metric { /** * Add a time event (usually in microseconds). @@ -37,8 +37,4 @@ public interface TimedMetric { */ TimedMetricStats collect(boolean reset); - /** - * Visit non empty metrics. - */ - void visit(MetricVisitor visitor); } diff --git a/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java b/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java index 5d10397ba2..aeff635e82 100644 --- a/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java +++ b/ebean-api/src/main/java/io/ebean/metric/TimedMetricMap.java @@ -5,7 +5,7 @@ /** * A map of timed metrics keyed by a string. */ -public interface TimedMetricMap { +public interface TimedMetricMap extends Metric { /** * Add a time event given the start nanos. @@ -17,8 +17,4 @@ public interface TimedMetricMap { */ void add(String key, long exeMicros); - /** - * Visit the metric. - */ - void visit(MetricVisitor visitor); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java index 2590265716..a0eea215b2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/cache/DefaultServerCache.java @@ -6,6 +6,7 @@ import io.ebean.cache.ServerCacheStatistics; import io.ebean.meta.MetricVisitor; import io.ebean.metric.CountMetric; +import io.ebean.metric.Metric; import io.ebean.metric.MetricFactory; import java.io.Serializable; @@ -48,6 +49,7 @@ public class DefaultServerCache implements ServerCache { protected final CountMetric idleCount; protected final CountMetric ttlCount; protected final CountMetric lruCount; + protected final Metric sizeCount; protected final String name; protected final String shortName; protected final int maxSize; @@ -81,6 +83,7 @@ public DefaultServerCache(DefaultServerCacheConfig config) { this.idleCount = factory.createCountMetric(prefix + shortName + ".idle"); this.ttlCount = factory.createCountMetric(prefix + shortName + ".ttl"); this.lruCount = factory.createCountMetric(prefix + shortName + ".lru"); + this.sizeCount = factory.createMetric(prefix + shortName + ".size", map::size); } @@ -103,6 +106,7 @@ public void visit(MetricVisitor visitor) { idleCount.visit(visitor); ttlCount.visit(visitor); lruCount.visit(visitor); + sizeCount.visit(visitor); } @Override 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 6631d3f99c..edfb261d61 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 @@ -16,6 +16,8 @@ import io.ebean.event.readaudit.ReadAuditLogger; import io.ebean.event.readaudit.ReadAuditPrepare; import io.ebean.meta.*; +import io.ebean.metric.Metric; +import io.ebean.metric.MetricRegistry; import io.ebean.migration.auto.AutoMigrationRunner; import io.ebean.plugin.BeanType; import io.ebean.plugin.Plugin; @@ -2304,6 +2306,9 @@ public void visitMetrics(MetricVisitor visitor) { persister.visitMetrics(visitor); } extraMetrics.visitMetrics(visitor); + for (Metric metric : MetricRegistry.registered()) { + metric.visit(visitor); + } visitor.visitEnd(); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/AbstractMetric.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/AbstractMetric.java new file mode 100644 index 0000000000..7c1400d50e --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/AbstractMetric.java @@ -0,0 +1,25 @@ +package io.ebeaninternal.server.profile; + +import io.ebean.meta.MetricVisitor; +import io.ebean.metric.Metric; + +/** + * Used to collect counter metrics. + */ +abstract class AbstractMetric implements Metric { + + final String name; + private String reportName; + + AbstractMetric(String name) { + this.name = name; + } + + String reportName(MetricVisitor visitor) { + if (reportName == null) { + this.reportName = visitor.namingConvention().apply(name); + } + return reportName; + } + +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetric.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetric.java index 34c47cfc64..d9c84bd067 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetric.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetric.java @@ -62,25 +62,4 @@ String reportName(MetricVisitor visitor) { return tmp; } - private static class DCountMetricStats implements CountMetricStats { - - private final String name; - private final long count; - - private DCountMetricStats(String name, long count) { - this.name = name; - this.count = count; - } - - @Override - public String name() { - return name; - } - - @Override - public long count() { - return count; - } - } - } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetricStats.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetricStats.java new file mode 100644 index 0000000000..1265c97ed9 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DCountMetricStats.java @@ -0,0 +1,27 @@ +package io.ebeaninternal.server.profile; + +import io.ebean.metric.CountMetricStats; + +/** + * Holder for count metric values. + */ +class DCountMetricStats implements CountMetricStats { + + private final String name; + private final long count; + + DCountMetricStats(String name, long count) { + this.name = name; + this.count = count; + } + + @Override + public String name() { + return name; + } + + @Override + public long count() { + return count; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DIntMetric.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DIntMetric.java new file mode 100644 index 0000000000..f4e777ea4b --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DIntMetric.java @@ -0,0 +1,36 @@ +package io.ebeaninternal.server.profile; + +import io.ebean.meta.MetricVisitor; +import io.ebean.metric.Metric; + +import java.util.function.IntSupplier; + +/** + * IntSupplier metric. + */ +final class DIntMetric implements Metric { + private final String name; + private final IntSupplier supplier; + private String reportName; + + DIntMetric(String name, IntSupplier supplier) { + this.name = name; + this.supplier = supplier; + } + + + @Override + public void visit(MetricVisitor visitor) { + int val = supplier.getAsInt(); + if (val != 0) { + final String name = reportName != null ? reportName : reportName(visitor); + visitor.visitCount(new DCountMetricStats(name, val)); + } + } + + String reportName(MetricVisitor visitor) { + final String tmp = visitor.namingConvention().apply(name); + this.reportName = tmp; + return tmp; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DLongMetric.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DLongMetric.java new file mode 100644 index 0000000000..ba291e4da4 --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DLongMetric.java @@ -0,0 +1,36 @@ +package io.ebeaninternal.server.profile; + +import io.ebean.meta.MetricVisitor; +import io.ebean.metric.Metric; + +import java.util.function.LongSupplier; + +/** + * LongSpplier metric. + */ +final class DLongMetric implements Metric { + private final String name; + private final LongSupplier supplier; + private String reportName; + + DLongMetric(String name, LongSupplier supplier) { + this.name = name; + this.supplier = supplier; + } + + + @Override + public void visit(MetricVisitor visitor) { + long val = supplier.getAsLong(); + if (val != 0) { + final String name = reportName != null ? reportName : reportName(visitor); + visitor.visitCount(new DCountMetricStats(name, val)); + } + } + + String reportName(MetricVisitor visitor) { + final String tmp = visitor.namingConvention().apply(name); + this.reportName = tmp; + return tmp; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java index d51ff2198d..5166524451 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/profile/DMetricFactory.java @@ -2,11 +2,15 @@ import io.ebean.ProfileLocation; import io.ebean.metric.CountMetric; +import io.ebean.metric.Metric; import io.ebean.metric.MetricFactory; import io.ebean.metric.QueryPlanMetric; import io.ebean.metric.TimedMetric; import io.ebean.metric.TimedMetricMap; +import java.util.function.IntSupplier; +import java.util.function.LongSupplier; + /** * Default metric factory implementation. */ @@ -27,6 +31,16 @@ public CountMetric createCountMetric(String name) { return new DCountMetric(name); } + @Override + public Metric createMetric(String name, LongSupplier supplier) { + return new DLongMetric(name, supplier); + } + + @Override + public Metric createMetric(String name, IntSupplier supplier) { + return new DIntMetric(name, supplier); + } + @Override public QueryPlanMetric createQueryPlanMetric(Class type, String label, ProfileLocation profileLocation, String sql) { return new DQueryPlanMetric(new DQueryPlanMeta(type, label, profileLocation, sql), new DTimedMetric(label)); From 53c77231b32d2c143779ffcdf317bd6d0c6892a4 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 16:17:48 +0200 Subject: [PATCH 17/36] NOPR - FOCONIS specific notes Release-infos Switch to FOCONIS Repo NOPR - FOCONIS Github Actions repo ebean-migration --- .github/workflows/build.yml | 9 ++- .github/workflows/db2luw.yml | 9 ++- .github/workflows/h2database.yml | 9 ++- .github/workflows/jdk-ea.yml | 9 ++- .github/workflows/mariadb.yml | 9 ++- .github/workflows/multi-db-platform.yml | 29 +++++++-- .github/workflows/multi-jdk-build.yml | 9 ++- .github/workflows/mysql.yml | 9 ++- .github/workflows/oracle.yml | 9 ++- .github/workflows/postgres.yml | 9 ++- .github/workflows/sqlserver-2019.yml | 9 ++- .github/workflows/sqlserver.yml | 9 ++- .github/workflows/yugabyte.yml | 9 ++- README.md | 33 +++++----- pom.xml | 6 +- release.md | 82 +++++++++++++++++++++++++ 16 files changed, 223 insertions(+), 35 deletions(-) create mode 100644 release.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66a1827851..41f4616f6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,10 +34,17 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: Maven version run: mvn --version # - name: Maven single test # run: mvn --batch-mode clean verify -Dtest="io.ebeaninternal.server.core.DefaultServer_getReferenceTest" -DfailIfNoTests=false - name: Build with Maven - run: mvn -T 8 clean test + run: mvn -T 8 clean test -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/db2luw.yml b/.github/workflows/db2luw.yml index f391e5c5b5..8b9ac14ce2 100644 --- a/.github/workflows/db2luw.yml +++ b/.github/workflows/db2luw.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: db2 - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-db2.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-db2.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/h2database.yml b/.github/workflows/h2database.yml index df34b9bbb9..af02c19394 100644 --- a/.github/workflows/h2database.yml +++ b/.github/workflows/h2database.yml @@ -34,8 +34,15 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: Maven version run: mvn --version - name: H2Database - run: mvn -T 8 clean package + run: mvn -T 8 clean package -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/jdk-ea.yml b/.github/workflows/jdk-ea.yml index 8e413bdd33..c7462c5320 100644 --- a/.github/workflows/jdk-ea.yml +++ b/.github/workflows/jdk-ea.yml @@ -34,8 +34,15 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: Maven version run: mvn --version - name: Build with Maven - run: mvn -T 8 test + run: mvn -T 8 test -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mariadb.yml b/.github/workflows/mariadb.yml index 53f7f4ade3..adbee38252 100644 --- a/.github/workflows/mariadb.yml +++ b/.github/workflows/mariadb.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: mariadb 10.6 - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mariadb.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/multi-db-platform.yml b/.github/workflows/multi-db-platform.yml index 5c021ba869..81e6d02fd2 100644 --- a/.github/workflows/multi-db-platform.yml +++ b/.github/workflows/multi-db-platform.yml @@ -31,18 +31,35 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: h2database - run: mvn clean test + run: mvn clean test -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: postgres - run: mvn clean test -Dprops.file=testconfig/ebean-postgres.properties + run: mvn clean test -Dprops.file=testconfig/ebean-postgres.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: mysql - run: mvn clean test -Dprops.file=testconfig/ebean-mysql.properties + run: mvn clean test -Dprops.file=testconfig/ebean-mysql.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: mariadb - run: mvn clean test -Dprops.file=testconfig/ebean-mariadb.properties + run: mvn clean test -Dprops.file=testconfig/ebean-mariadb.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: yugabyte - run: mvn clean test -Dprops.file=testconfig/ebean-yugabyte.properties + run: mvn clean test -Dprops.file=testconfig/ebean-yugabyte.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: sqlserver - run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver17.properties + run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver17.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # - name: sqlserver19 # run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver19.properties # - name: db2 diff --git a/.github/workflows/multi-jdk-build.yml b/.github/workflows/multi-jdk-build.yml index 80289b0bc2..d407e2b0f4 100644 --- a/.github/workflows/multi-jdk-build.yml +++ b/.github/workflows/multi-jdk-build.yml @@ -34,8 +34,15 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: Maven version run: mvn --version - name: Build with Maven - run: mvn package + run: mvn package -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mysql.yml b/.github/workflows/mysql.yml index 75988e08a2..fc23cac876 100644 --- a/.github/workflows/mysql.yml +++ b/.github/workflows/mysql.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: mysql - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mysql.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-mysql.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/oracle.yml b/.github/workflows/oracle.yml index 3b49da1a77..07309672ec 100644 --- a/.github/workflows/oracle.yml +++ b/.github/workflows/oracle.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: oracle - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-oracle.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-oracle.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/postgres.yml b/.github/workflows/postgres.yml index 44a2f8aecf..9e409edf6b 100644 --- a/.github/workflows/postgres.yml +++ b/.github/workflows/postgres.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: postgres - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-postgres.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-postgres.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sqlserver-2019.yml b/.github/workflows/sqlserver-2019.yml index a14e519963..1c43c3bbb8 100644 --- a/.github/workflows/sqlserver-2019.yml +++ b/.github/workflows/sqlserver-2019.yml @@ -31,5 +31,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: sqlserver 2019 latest - run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver19.properties + run: mvn clean test -Dprops.file=testconfig/ebean-sqlserver19.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sqlserver.yml b/.github/workflows/sqlserver.yml index 7244170c0d..d24467e161 100644 --- a/.github/workflows/sqlserver.yml +++ b/.github/workflows/sqlserver.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: sqlserver 2017 - run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver17.properties + run: mvn -T 8 clean test -Dprops.file=testconfig/ebean-sqlserver17.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/yugabyte.yml b/.github/workflows/yugabyte.yml index 80a76e801a..d832653f35 100644 --- a/.github/workflows/yugabyte.yml +++ b/.github/workflows/yugabyte.yml @@ -34,5 +34,12 @@ jobs: path: ~/.m2 key: build-${{ env.cache-name }} + - name: maven-settings + uses: s4u/maven-settings-action@v2 + with: + servers: '[{"id": "github-release", "username": "dummy", "password": "${GITHUB_TOKEN}"}]' + githubServer: false - name: yugabyte - run: mvn clean test -Dprops.file=testconfig/ebean-yugabyte.properties + run: mvn clean test -Dprops.file=testconfig/ebean-yugabyte.properties -Pgithub + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 732345e169..5c965e78f7 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,26 @@ -[![Build](https://github.com/ebean-orm/ebean/actions/workflows/build.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/build.yml) +[![Build](https://github.com/FOCONIS/ebean/actions/workflows/build.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/build.yml) [![Maven Central : ebean](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.ebean/ebean) -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ebean-orm/ebean/blob/master/LICENSE) -[![Multi-JDK Build](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/multi-jdk-build.yml) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/FOCONIS/ebean/blob/master/LICENSE) +[![Multi-JDK Build](https://github.com/FOCONIS/ebean/actions/workflows/multi-jdk-build.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/multi-jdk-build.yml) ##### Build with database platforms -[![H2Database](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/h2database.yml) -[![Postgres](https://github.com/ebean-orm/ebean/actions/workflows/postgres.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/postgres.yml) -[![MySql](https://github.com/ebean-orm/ebean/actions/workflows/mysql.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/mysql.yml) -[![MariaDB](https://github.com/ebean-orm/ebean/actions/workflows/mariadb.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/mariadb.yml) -[![Oracle](https://github.com/ebean-orm/ebean/actions/workflows/oracle.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/oracle.yml) -[![SqlServer](https://github.com/ebean-orm/ebean/actions/workflows/sqlserver.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/sqlserver.yml) -[![DB2 LUW](https://github.com/ebean-orm/ebean/actions/workflows/db2luw.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/db2luw.yml) -[![Yugabyte](https://github.com/ebean-orm/ebean/actions/workflows/yugabyte.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/yugabyte.yml) +[![H2Database](https://github.com/FOCONIS/ebean/actions/workflows/h2database.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/h2database.yml) +[![Postgres](https://github.com/FOCONIS/ebean/actions/workflows/postgres.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/postgres.yml) +[![MySql](https://github.com/FOCONIS/ebean/actions/workflows/mysql.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/mysql.yml) +[![MariaDB](https://github.com/FOCONIS/ebean/actions/workflows/mariadb.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/mariadb.yml) +[![Oracle](https://github.com/FOCONIS/ebean/actions/workflows/oracle.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/oracle.yml) +[![SqlServer](https://github.com/FOCONIS/ebean/actions/workflows/sqlserver.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/sqlserver.yml) +[![DB2 LUW](https://github.com/FOCONIS/ebean/actions/workflows/db2luw.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/db2luw.yml) +[![Yugabyte](https://github.com/FOCONIS/ebean/actions/workflows/yugabyte.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/yugabyte.yml) + ##### Build with Java Early Access versions -[![ebean EA](https://github.com/ebean-orm/ebean/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean/actions/workflows/jdk-ea.yml) -[![datasource EA](https://github.com/ebean-orm/ebean-datasource/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-datasource/actions/workflows/jdk-ea.yml) -[![migration EA](https://github.com/ebean-orm/ebean-migration/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-migration/actions/workflows/jdk-ea.yml) -[![test-docker EA](https://github.com/ebean-orm/ebean-test-docker/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-test-docker/actions/workflows/jdk-ea.yml) -[![ebean-agent EA](https://github.com/ebean-orm/ebean-agent/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/ebean-orm/ebean-agent/actions/workflows/jdk-ea.yml) +[![ebean EA](https://github.com/FOCONIS/ebean/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/FOCONIS/ebean/actions/workflows/jdk-ea.yml) +[![datasource EA](https://github.com/FOCONIS/ebean-datasource/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/FOCONIS/ebean-datasource/actions/workflows/jdk-ea.yml) +[![migration EA](https://github.com/FOCONIS/ebean-migration/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/FOCONIS/ebean-migration/actions/workflows/jdk-ea.yml) +[![test-docker EA](https://github.com/FOCONIS/ebean-test-docker/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/FOCONIS/ebean-test-docker/actions/workflows/jdk-ea.yml) +[![ebean-agent EA](https://github.com/FOCONIS/ebean-agent/actions/workflows/jdk-ea.yml/badge.svg)](https://github.com/FOCONIS/ebean-agent/actions/workflows/jdk-ea.yml) ---------------------- diff --git a/pom.xml b/pom.xml index e6f6ead660..f8f1106c49 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ https://ebean.io/ - scm:git:git@github.com:ebean-orm/ebean.git + scm:git:git@github.com:FOCONIS/ebean.git HEAD @@ -155,6 +155,10 @@ github-release https://maven.pkg.github.com/foconis/ebean-agent + + github-release-migration + https://maven.pkg.github.com/foconis/ebean-migration + diff --git a/release.md b/release.md new file mode 100644 index 0000000000..b4f02041e6 --- /dev/null +++ b/release.md @@ -0,0 +1,82 @@ +## Merge back Robs master: + + +```bash +# First sync version to non-foc version. Version must be equal with rob's branch: +mvn versions:set -DgenerateBackupPoms=false -DnewVersion=13.x.x-SNAPSHOT +git commit -am "Sync version to upstream" + +# add remote (one time step) +git remote add upstream git@github.com:ebean-orm/ebean.git + +git fetch upstream +git merge upstream/master +``` +Now resolve all merge conflicts + +```bash +# Set back to foc-version +mvn versions:set -DgenerateBackupPoms=false -DnewVersion=13.x.x-FOCx-SNAPSHOT +``` + +Then check, if all -SNAPSHOT versions are foc-version + + +## Release command + + +Note: Since Jakarta, Ebean is no longer released with the release-plugin by rob. + + +Solution1: Steps must be done manually: +```bash +# Set Version to next version with +mvn versions:set -DgenerateBackupPoms=false -DnewVersion=13.x.x-FOCx +# commit everything +git add '*.xml' +git commit -m "bump version to release" +git tag 13.x.x-FOCx + +#build and deploy it +mvn clean source:jar deploy -DskipTests -Pfoconis -T 8 + +mvn versions:set -DgenerateBackupPoms=false -DnewVersion=13.x.x-FOCx+1-SNAPSHOT +git add '*.xml' +git commit -m "bump version to snapshot" +```` + + +Solution 2: Ensure that you are on a snapshot version and use + +```bash +mvn release:prepare release:perform -Darguments="-Dgpg.skip -DskipTests" -Pfoconis +``` + +### Build releases for github + +First, checkout latest release commit with +```bash +git checkout HEAD~1 +``` + +Build github release: + +```bash +mvn clean source:jar deploy -DskipTests -Pgithub -T 8 +``` + +## Fix POMs after release + +After a release, you may have to fix poms with + +```bash +mvn versions:update-parent -DallowSnapshots=true -DgenerateBackupPoms=false -Pjdk16plus -Pjdk15less +``` + + # wenn es Probleme mit Versionen gibt, dann manuell ebean-kotlin/pom.xml, tests/test-java16/pom.xml und tests/test-kotlin/pom.xml, usw. anpassen + # wenn es bei kotlin-querybean-generator krachts, dann den Modul auskommentieren oder Modul auslassen und mit ... -rf :NÄCHSTE-MODUL weitermachen + +## generate Java classes from .xsd: + + export JAVA_TOOL_OPTIONS="-Duser.language=en -Duser.country=US -Dfile.encoding=UTF-8" + /c/Program\ Files/Java/jdk1.8.0_201/bin/xjc.exe src/main/resources/ebean-dbmigration-1.0.xsd -d src/main/java -p io.ebeaninternal.dbmigration.migration From cbf8dd24f63d0144dc214265b36752bacc2eccd5 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 20 Sep 2023 16:28:38 +0200 Subject: [PATCH 18/36] NOPR ADD readonly check on markdirty --- .../io/ebean/bean/InterceptReadWrite.java | 50 +++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java index 72d9b9bd9f..e392ef0580 100644 --- a/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java +++ b/ebean-api/src/main/java/io/ebean/bean/InterceptReadWrite.java @@ -1,26 +1,24 @@ package io.ebean.bean; +import io.avaje.applog.AppLog; import io.ebean.DB; import io.ebean.Database; import io.ebean.ValuePair; - import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.PersistenceException; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.net.URL; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import static java.lang.System.Logger.Level.WARNING; + /** * This is the object added to every entity bean using byte code enhancement. *

@@ -30,6 +28,8 @@ public final class InterceptReadWrite extends InterceptBase implements EntityBeanIntercept { private static final long serialVersionUID = 1834735632647183821L; + public static final System.Logger log = AppLog.getLogger("io.ebean.bean"); + private static final int STATE_NEW = 0; private static final int STATE_REFERENCE = 1; private static final int STATE_LOADED = 2; @@ -51,6 +51,11 @@ public final class InterceptReadWrite extends InterceptBase implements EntityBea */ private static final byte FLAG_MUTABLE_HASH_SET = 16; + /** + * Flag indicates that warning was logged. + */ + private static final byte FLAG_MUTABLE_WARN_LOGGED = 32; + private final ReentrantLock lock = new ReentrantLock(); private transient NodeUsageCollector nodeUsageCollector; private transient PersistenceContext persistenceContext; @@ -226,8 +231,17 @@ public boolean isDirty() { } if (mutableInfo != null) { for (int i = 0; i < mutableInfo.length; i++) { + if ((flags[i] & FLAG_MUTABLE_WARN_LOGGED) == FLAG_MUTABLE_WARN_LOGGED) { + break; // do not check again and do NOT mark as dirty + } if (mutableInfo[i] != null && !mutableInfo[i].isEqualToObject(value(i))) { - dirty = true; + if (readOnly) { + log.log(WARNING, "Mutable object in {0}.{1} ({2}) changed. Not setting bean dirty, because it is readonly", + owner.getClass().getName(), property(i), owner); + flags[i] |= FLAG_MUTABLE_WARN_LOGGED; + } else { + dirty = true; + } break; } } @@ -237,12 +251,16 @@ public boolean isDirty() { @Override public void setEmbeddedDirty(int embeddedProperty) { + checkReadonly(); this.dirty = true; setEmbeddedPropertyDirty(embeddedProperty); } @Override public void setDirty(boolean dirty) { + if (dirty) { + checkReadonly(); + } this.dirty = dirty; } @@ -842,18 +860,14 @@ public void preSetterMany(boolean interceptField, int propertyIndex, Object oldV if (state == STATE_NEW) { setLoadedProperty(propertyIndex); } else { - if (readOnly) { - throw new IllegalStateException("This bean is readOnly"); - } + checkReadonly(); setChangeLoaded(propertyIndex); } } @Override public void setChangedPropertyValue(int propertyIndex, boolean setDirtyState, Object origValue) { - if (readOnly) { - throw new IllegalStateException("This bean is readOnly"); - } + checkReadonly(); setChangedProperty(propertyIndex); if (setDirtyState) { setOriginalValue(propertyIndex, origValue); @@ -864,6 +878,7 @@ public void setChangedPropertyValue(int propertyIndex, boolean setDirtyState, Ob @Override public void setDirtyStatus() { if (!dirty) { + checkReadonly(); dirty = true; if (embeddedOwner != null) { // Cascade dirty state from Embedded bean to parent bean @@ -875,6 +890,12 @@ public void setDirtyStatus() { } } + private void checkReadonly() { + if (readOnly) { + throw new IllegalStateException("This bean is readOnly"); + } + } + @Override public void preSetter(boolean intercept, int propertyIndex, Object oldValue, Object newValue) { if (state == STATE_NEW) { @@ -1036,6 +1057,7 @@ public boolean isChangedProp(int i) { } else if (mutableInfo == null || mutableInfo[i] == null || mutableInfo[i].isEqualToObject(value(i))) { return false; } else { + checkReadonly(); // mark for change flags[i] |= FLAG_CHANGED_PROP; dirty = true; // this makes the bean automatically dirty! From ff0ee0095c0dd1f06c1965011b6000aade9b9923 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Fri, 8 Dec 2023 14:17:16 +0100 Subject: [PATCH 19/36] FIX: nested NOT_SUPPORTED transaction with an inner REQUIRES transaction --- .../transaction/TransactionManager.java | 11 ++------- .../transaction/TestNestedTransaction.java | 23 ++++++++++++++++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java index 821d92094e..2d024f94a6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/transaction/TransactionManager.java @@ -28,8 +28,8 @@ import io.ebeanservice.docstore.api.DocStoreTransaction; import io.ebeanservice.docstore.api.DocStoreUpdateProcessor; import io.ebeanservice.docstore.api.DocStoreUpdates; - import jakarta.persistence.PersistenceException; + import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @@ -176,13 +176,6 @@ public final SpiTransaction active() { return scopeManager.active(); } - /** - * Return the current active transaction as a scoped transaction. - */ - private ScopedTransaction activeScoped() { - return (ScopedTransaction) scopeManager.active(); - } - /** * Return the current transaction from thread local scope. Note that it may be inactive. */ @@ -499,7 +492,7 @@ public final ScopedTransaction externalBeginTransaction(SpiTransaction transacti */ public final ScopedTransaction beginScopedTransaction(TxScope txScope) { txScope = initTxScope(txScope); - ScopedTransaction txnContainer = activeScoped(); + ScopedTransaction txnContainer = (ScopedTransaction) inScope(); boolean setToScope; boolean nestedSavepoint; diff --git a/ebean-test/src/test/java/org/tests/transaction/TestNestedTransaction.java b/ebean-test/src/test/java/org/tests/transaction/TestNestedTransaction.java index b2bf41825c..e7314fad4c 100644 --- a/ebean-test/src/test/java/org/tests/transaction/TestNestedTransaction.java +++ b/ebean-test/src/test/java/org/tests/transaction/TestNestedTransaction.java @@ -1,8 +1,9 @@ package org.tests.transaction; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.Transaction; +import io.ebean.TxScope; +import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -235,4 +236,24 @@ public void testNested_111() { } assertModified(); } + + @Test + public void test_txn_with_not_supported() { + + try (Transaction txn1 = DB.beginTransaction()) { + assertThat(getInScopeTransaction()).isNotNull(); + getInScopeTransaction().putUserObject("foo", "bar"); + + try (Transaction txn2 = DB.beginTransaction(TxScope.notSupported())) { + // pause txn1 + try (Transaction txn3 = DB.beginTransaction()) { + // create a new Txn scope + txn3.commit(); + } + txn2.commit(); + } + // resume txn1 + assertThat(getInScopeTransaction().getUserObject("foo")).isEqualTo("bar"); + } + } } From 2cc25211604523da9461c3003636f312e1f30ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Fr=C3=B6hler?= Date: Mon, 1 Jul 2024 16:56:21 +0200 Subject: [PATCH 20/36] Throw specific LengthCheckException instead of DataIntegrityException Fix TestLength test --- .../main/java/io/ebean/LengthCheckException.java | 15 +++++++++++++++ .../ebeaninternal/server/deploy/BeanProperty.java | 3 ++- .../src/test/java/org/tests/basic/TestLength.java | 3 ++- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/LengthCheckException.java diff --git a/ebean-api/src/main/java/io/ebean/LengthCheckException.java b/ebean-api/src/main/java/io/ebean/LengthCheckException.java new file mode 100644 index 0000000000..b66bfb51c7 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/LengthCheckException.java @@ -0,0 +1,15 @@ +package io.ebean; + +/** + * Exception when the length check has an error. + */ +public class LengthCheckException extends DataIntegrityException { + private static final long serialVersionUID = -4771932723285724817L; + + /** + * Create with a message. + */ + public LengthCheckException(String message) { + super(message); + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index feeec9e07b..712a27455f 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonToken; import io.ebean.DataIntegrityException; +import io.ebean.LengthCheckException; import io.ebean.ValuePair; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; @@ -573,7 +574,7 @@ public void bind(DataBind b, Object value) throws SQLException { if (s.length() > 50) { s = s.substring(0, 47) + "..."; } - throw new DataIntegrityException("Cannot bind value '" + s + "' (effective length=" + length + ") to column '" + dbColumn + "' (length=" + dbLength + ")"); + throw new LengthCheckException("Cannot bind value '" + s + "' (effective length=" + length + ") to column '" + dbColumn + "' (length=" + dbLength + ")"); } } } diff --git a/ebean-test/src/test/java/org/tests/basic/TestLength.java b/ebean-test/src/test/java/org/tests/basic/TestLength.java index fa30635f95..997a07b78f 100644 --- a/ebean-test/src/test/java/org/tests/basic/TestLength.java +++ b/ebean-test/src/test/java/org/tests/basic/TestLength.java @@ -2,6 +2,7 @@ import io.ebean.DB; import io.ebean.DataIntegrityException; +import io.ebean.LengthCheckException; import io.ebean.xtest.BaseTestCase; import org.junit.jupiter.api.Test; import org.tests.model.json.EBasicJsonList; @@ -92,7 +93,7 @@ void testUtf8() { assertThatThrownBy(() -> { // we expect, that we can NOT save the bean, this is ensured by the bind validator. DB.save(bean); - }).isInstanceOf(DataIntegrityException.class); + }).isInstanceOf(LengthCheckException.class); } else { DB.save(bean); } From b58c6c9211ca4ceaed1330fc836b2b3bbd7dc437 Mon Sep 17 00:00:00 2001 From: Noemi Praml <63635801+nPraml@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:23:19 +0200 Subject: [PATCH 21/36] Transaction API extended with root transaction (#102) --- .../src/main/java/io/ebean/Transaction.java | 8 ++++++ .../api/SpiTransactionProxy.java | 6 ++++ .../transaction/TestNestedTransaction.java | 28 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/ebean-api/src/main/java/io/ebean/Transaction.java b/ebean-api/src/main/java/io/ebean/Transaction.java index adaadf00c1..3347b250c4 100644 --- a/ebean-api/src/main/java/io/ebean/Transaction.java +++ b/ebean-api/src/main/java/io/ebean/Transaction.java @@ -561,4 +561,12 @@ static Transaction current() { * Get an object added with {@link #putUserObject(String, Object)}. */ Object getUserObject(String name); + + /** + * In case of nested transaction, this returns the root transaction. + * @return + */ + default Transaction root() { + return this; + } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java index 7b79a3f5c4..183ede3c22 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiTransactionProxy.java @@ -1,6 +1,7 @@ package io.ebeaninternal.api; import io.ebean.ProfileLocation; +import io.ebean.Transaction; import io.ebean.TransactionCallback; import io.ebean.annotation.DocStoreMode; import io.ebean.event.changelog.BeanChange; @@ -462,4 +463,9 @@ public void postRollback(Throwable cause) { public void deactivateExternal() { transaction.deactivateExternal(); } + + @Override + public Transaction root() { + return transaction.root(); + } } diff --git a/ebean-test/src/test/java/org/tests/transaction/TestNestedTransaction.java b/ebean-test/src/test/java/org/tests/transaction/TestNestedTransaction.java index e7314fad4c..42108d78d1 100644 --- a/ebean-test/src/test/java/org/tests/transaction/TestNestedTransaction.java +++ b/ebean-test/src/test/java/org/tests/transaction/TestNestedTransaction.java @@ -256,4 +256,32 @@ public void test_txn_with_not_supported() { assertThat(getInScopeTransaction().getUserObject("foo")).isEqualTo("bar"); } } + + @Test + public void test_txn_putUserObjectInRoot() { + + try (Transaction txn1 = DB.beginTransaction(TxScope.requiresNew())) { + try (Transaction txn2 = DB.beginTransaction()) { + for (int i = 0; i < 2; i++) { + + try (Transaction txn3 = DB.beginTransaction()) { + Object x = Transaction.current().root().getUserObject("x"); + Object y = Transaction.current().getUserObject("y"); + if (i == 0) { + assertThat(x).isNull(); + assertThat(y).isNull(); + } else { + assertThat(x).isEqualTo(2); + assertThat(y).isNull(); + } + Transaction.current().root().putUserObject("x", 2); + Transaction.current().putUserObject("y", 3); + txn3.commit(); + } + } + txn2.commit(); + } + } + } + } From 5c83c833f83549757210758e19417466afbdbde1 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 16:09:08 +0200 Subject: [PATCH 22/36] NOPR - NEW: MergeBeans function instead of updating existing bean from json --- .../main/java/io/ebean/BeanMergeOptions.java | 126 ++++++++++++ .../src/main/java/io/ebean/Database.java | 6 + .../java/io/ebean/bean/BeanCollection.java | 10 +- .../main/java/io/ebean/common/BeanList.java | 5 +- .../main/java/io/ebean/common/BeanMap.java | 5 +- .../main/java/io/ebean/common/BeanSet.java | 5 +- .../io/ebean/text/json/JsonBeanReader.java | 11 +- .../java/io/ebean/text/json/JsonContext.java | 13 ++ .../ebeaninternal/api/json/SpiJsonReader.java | 1 - .../server/core/DefaultServer.java | 6 + .../server/deploy/BeanDescriptor.java | 38 ++-- .../deploy/BeanDescriptorElementEmbedded.java | 8 +- .../BeanDescriptorElementEmbeddedMap.java | 4 +- .../server/deploy/BeanDescriptorJsonHelp.java | 26 +-- .../server/deploy/BeanMergeHelp.java | 192 ++++++++++++++++++ .../server/deploy/BeanProperty.java | 18 +- .../server/deploy/BeanPropertyAssocMany.java | 110 ++++------ .../deploy/BeanPropertyAssocManyJsonHelp.java | 7 +- .../server/deploy/BeanPropertyAssocOne.java | 42 ++-- .../deploy/BeanPropertySimpleCollection.java | 2 +- .../server/json/DJsonBeanReader.java | 4 +- .../server/json/DJsonContext.java | 52 ++--- .../ebeaninternal/server/json/ReadJson.java | 25 +-- .../ebeaninternal/server/query/STreeType.java | 2 +- .../server/query/SqlTreeLoadBean.java | 2 +- .../server/deploy/BeanDescriptorTest.java | 2 +- .../xtest/internal/api/TDSpiEbeanServer.java | 5 + .../ebean/xtest/internal/api/TDSpiServer.java | 6 + .../java/org/tests/model/basic/Customer.java | 2 +- .../json/TestJsonBeanDescriptorParse.java | 115 ++++++++++- .../text/json/TestTextJsonInheritance.java | 77 ++++++- 31 files changed, 699 insertions(+), 228 deletions(-) create mode 100644 ebean-api/src/main/java/io/ebean/BeanMergeOptions.java create mode 100644 ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMergeHelp.java diff --git a/ebean-api/src/main/java/io/ebean/BeanMergeOptions.java b/ebean-api/src/main/java/io/ebean/BeanMergeOptions.java new file mode 100644 index 0000000000..daa8dd2657 --- /dev/null +++ b/ebean-api/src/main/java/io/ebean/BeanMergeOptions.java @@ -0,0 +1,126 @@ +package io.ebean; + +import io.ebean.bean.PersistenceContext; +import io.ebean.plugin.Property; + +/** + * Merge options, when merging two beans. You can fine tune, how the merge should happen. + * For example you can exclude some special properties or write your custom merge handler. + * + * @author Roland Praml, FOCONIS AG + */ +public class BeanMergeOptions { + + /** + * Interface to write your own merge handler. + * + * @param + */ + @FunctionalInterface + public interface MergeHandler { + /** + * The new bean and the existing is passed. Together with property + * and path, you can decide, if you want to continue with merge or not. + */ + boolean mergeBeans(T bean, T existing, Property property, String path); + + } + + private PersistenceContext persistenceContext; + + private MergeHandler mergeHandler; + + private boolean mergeId = true; + + private boolean mergeVersion = false; + + private boolean clearCollections = true; + + private boolean addExistingToPersistenceContext = true; + + /** + * Return the persistence context, that is used during merge. + * If no one is specified, the persistence context of the bean will be used + */ + public PersistenceContext getPersistenceContext() { + return persistenceContext; + } + + /** + * Sets the persistence context, that is used during merge. + * * If no one is specified, the persistence context of the bean will be used + */ + public void setPersistenceContext(PersistenceContext persistenceContext) { + this.persistenceContext = persistenceContext; + } + + /** + * Returns the merge handler, if you want to do special handling for some properties. + */ + public MergeHandler getMergeHandler() { + return mergeHandler; + } + + /** + * Sets the merge handler, if you want to do special handling for some properties. + */ + public void setMergeHandler(MergeHandler mergeHandler) { + this.mergeHandler = mergeHandler; + } + + /** + * Returns if we should merge the ID property (default=true). + */ + public boolean isMergeId() { + return mergeId; + } + + /** + * Should we merge the ID property (default=true). + */ + public void setMergeId(boolean mergeId) { + this.mergeId = mergeId; + } + + /** + * Returns if we should merge the version property (default=false). + */ + public boolean isMergeVersion() { + return mergeVersion; + } + + /** + * Should we merge the version property (default=false). + */ + public void setMergeVersion(boolean mergeVersion) { + this.mergeVersion = mergeVersion; + } + + /** + * Returns if we should clear/replace beanCollections (default=true). + */ + public boolean isClearCollections() { + return clearCollections; + } + + /** + * Should we clear/replace beanCollections (default=true). + */ + public void setClearCollections(boolean clearCollections) { + this.clearCollections = clearCollections; + } + + /** + * Returns if we should add existing beans to the persistenceContext (default=true). + */ + public boolean isAddExistingToPersistenceContext() { + return addExistingToPersistenceContext; + } + + /** + * Should we add existing beans to the persistenceContext (default=true). + */ + public void setAddExistingToPersistenceContext(boolean addExistingToPersistenceContext) { + this.addExistingToPersistenceContext = addExistingToPersistenceContext; + } +} diff --git a/ebean-api/src/main/java/io/ebean/Database.java b/ebean-api/src/main/java/io/ebean/Database.java index d0d747d8e3..853a90b287 100644 --- a/ebean-api/src/main/java/io/ebean/Database.java +++ b/ebean-api/src/main/java/io/ebean/Database.java @@ -1184,6 +1184,12 @@ default Set checkUniqueness(Object bean, Transaction transaction) { */ void merge(Object bean, MergeOptions options, Transaction transaction); + /** + * Merges two beans (without saving them). All modified properties from bean are copied to existing. + * Returns existing bean. If null is passed, a new instance of bean is retuned. + */ + T mergeBeans(T bean, T existing, BeanMergeOptions options); + /** * Insert the bean. *

diff --git a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java index 4c07e9da45..dcb002266f 100644 --- a/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java +++ b/ebean-api/src/main/java/io/ebean/bean/BeanCollection.java @@ -162,8 +162,11 @@ enum ModifyListenMode { *

* For maps this returns the entrySet as we need the keys of the map. */ - Collection actualEntries(); + Collection actualEntries(boolean load); + default Collection actualEntries() { + return actualEntries(false); + } /** * Returns entries, that were lazily added at the end of the list. Might be null. */ @@ -247,4 +250,9 @@ default Collection getLazyAddedEntries(boolean reset) { * Return a shallow copy of this collection that is modifiable. */ BeanCollection shallowCopy(); + + /** + * Clears the underlying collection. + */ + void clear(); } diff --git a/ebean-api/src/main/java/io/ebean/common/BeanList.java b/ebean-api/src/main/java/io/ebean/common/BeanList.java index a1ecb1d4ec..8fa03659fb 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanList.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanList.java @@ -164,7 +164,10 @@ public Collection actualDetails() { } @Override - public Collection actualEntries() { + public Collection actualEntries(boolean load) { + if (load) { + init(); + } return list; } diff --git a/ebean-api/src/main/java/io/ebean/common/BeanMap.java b/ebean-api/src/main/java/io/ebean/common/BeanMap.java index fe959c22c0..d1c5801e38 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanMap.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanMap.java @@ -187,7 +187,10 @@ public Collection actualDetails() { * Returns the map entrySet. */ @Override - public Collection actualEntries() { + public Collection actualEntries(boolean load) { + if (load) { + init(); + } return map.entrySet(); } diff --git a/ebean-api/src/main/java/io/ebean/common/BeanSet.java b/ebean-api/src/main/java/io/ebean/common/BeanSet.java index dda2b24ee9..d0d890f8b6 100644 --- a/ebean-api/src/main/java/io/ebean/common/BeanSet.java +++ b/ebean-api/src/main/java/io/ebean/common/BeanSet.java @@ -165,7 +165,10 @@ public Collection actualDetails() { } @Override - public Collection actualEntries() { + public Collection actualEntries(boolean load) { + if (load) { + init(); + } return set; } diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java index 935b788600..7acdb976f1 100644 --- a/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java +++ b/ebean-api/src/main/java/io/ebean/text/json/JsonBeanReader.java @@ -12,18 +12,11 @@ */ public interface JsonBeanReader { - /** - * Read the JSON into given bean. Will update existing properties. - */ - T read(T target); - /** * Read the JSON returning a bean. */ - default T read() { - return read(null); - } - + T read(); + /** * Create a new reader taking the context from the existing one but using a new JsonParser. */ diff --git a/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java b/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java index d2c0df822b..3c31f11667 100644 --- a/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java +++ b/ebean-api/src/main/java/io/ebean/text/json/JsonContext.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import io.ebean.BeanMergeOptions; import io.ebean.FetchPath; import io.ebean.plugin.BeanType; @@ -64,7 +65,9 @@ public interface JsonContext { * instances, so the object identity will not be preserved here. * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, JsonParser)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, JsonParser parser) throws JsonIOException; /** @@ -72,7 +75,9 @@ public interface JsonContext { * See {@link #toBean(Class, JsonParser)} for details modified. * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, JsonParser, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException; /** @@ -80,7 +85,9 @@ public interface JsonContext { * See {@link #toBean(Class, JsonParser)} for details * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, Reader)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, Reader json) throws JsonIOException; /** @@ -88,7 +95,9 @@ public interface JsonContext { * See {@link #toBean(Class, JsonParser)} for details modified. * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, Reader, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, Reader json, JsonReadOptions options) throws JsonIOException; /** @@ -96,7 +105,9 @@ public interface JsonContext { * See {@link #toBean(Class, JsonParser)} for details * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, String)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, String json) throws JsonIOException; /** @@ -104,7 +115,9 @@ public interface JsonContext { * See {@link #toBean(Class, JsonParser)} for details * * @throws JsonIOException When IOException occurs + * @deprecated use {@link #toBean(Class, String, JsonReadOptions)} and {@link io.ebean.Database#mergeBeans(Object, Object, BeanMergeOptions)} */ + @Deprecated void toBean(T target, String json, JsonReadOptions options) throws JsonIOException; /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java index 613b105cc6..4ed612defc 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/json/SpiJsonReader.java @@ -34,5 +34,4 @@ public interface SpiJsonReader { Object readValueUsingObjectMapper(Class propertyType) throws IOException; - boolean update(); } 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 edfb261d61..327ff1ef5a 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 @@ -817,6 +817,12 @@ public void merge(Object bean, MergeOptions options, @Nullable Transaction trans executeInTrans((txn) -> persister.merge(desc, checkEntityBean(bean), options, txn), transaction); } + @Override + public T mergeBeans(T bean, T existing, BeanMergeOptions options) { + BeanDescriptor desc = desc(bean.getClass()); + return (T) desc.mergeBeans(checkEntityBean(bean), (EntityBean) existing, options); + } + @Override public void lock(Object bean) { BeanDescriptor desc = desc(bean.getClass()); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index dcebbce4be..e4a8d36b34 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -695,22 +695,15 @@ public void metricPersistNoBatch(PersistRequest.Type type, long startNanos) { iudMetrics.addNoBatch(type, startNanos); } - public void merge(EntityBean bean, EntityBean existing) { - EntityBeanIntercept fromEbi = bean._ebean_getIntercept(); - EntityBeanIntercept toEbi = existing._ebean_getIntercept(); - int propertyLength = toEbi.propertyLength(); - String[] names = properties(); - for (int i = 0; i < propertyLength; i++) { - if (fromEbi.isLoadedProperty(i)) { - BeanProperty property = beanProperty(names[i]); - if (!toEbi.isLoadedProperty(i)) { - Object val = property.getValue(bean); - property.setValue(existing, val); - } else if (property.isMany()) { - property.merge(bean, existing); - } - } + /** + * Copies all modified fields from bean to existing. + * It returns normally the existing bean (or a new instance, if it was null) + */ + public EntityBean mergeBeans(EntityBean bean, EntityBean existing, BeanMergeOptions options) { + if (existing == null) { + existing = createEntityBean(); } + return new BeanMergeHelp(existing, options).mergeBeans(this, bean, existing); } /** @@ -1755,7 +1748,7 @@ public EntityBean createEntityBeanForJson() { /** * We actually need to do a query because we don't know the type without the discriminator value. */ - private T findReferenceBean(Object id, PersistenceContext pc) { + public T findReferenceBean(Object id, PersistenceContext pc) { DefaultOrmQuery query = new DefaultOrmQuery<>(this, ebeanServer, ebeanServer.expressionFactory()); query.setPersistenceContext(pc); return query.setId(id).findOne(); @@ -2025,8 +2018,8 @@ public void contextPut(PersistenceContext pc, Object id, Object bean) { * Put the bean into the persistence context if it is absent. */ @Override - public Object contextPutIfAbsent(PersistenceContext pc, Object id, EntityBean localBean) { - return pc.putIfAbsent(rootBeanType, id, localBean); + public EntityBean contextPutIfAbsent(PersistenceContext pc, Object id, EntityBean localBean) { + return (EntityBean) pc.putIfAbsent(rootBeanType, id, localBean); } /** @@ -3379,12 +3372,13 @@ void jsonWriteProperties(SpiJsonWriter writeJson, EntityBean bean) { jsonHelp.jsonWriteProperties(writeJson, bean); } - public T jsonRead(SpiJsonReader jsonRead, String path, T target) throws IOException { - return jsonHelp.jsonRead(jsonRead, path, true, target); + public T jsonRead(SpiJsonReader jsonRead, String path) throws IOException { + return jsonHelp.jsonRead(jsonRead, path, true); } - T jsonReadObject(SpiJsonReader jsonRead, String path, T target) throws IOException { - return jsonHelp.jsonRead(jsonRead, path, false, target); + + T jsonReadObject(SpiJsonReader jsonRead, String path) throws IOException { + return jsonHelp.jsonRead(jsonRead, path, false); } public List uniqueProps() { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java index 71d79b9dce..6a4517fd72 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbedded.java @@ -61,13 +61,13 @@ public void jsonWriteElement(SpiJsonWriter ctx, Object element) { } @Override - public T jsonRead(SpiJsonReader jsonRead, String path, T target) throws IOException { - return readJsonElement(jsonRead, path, target); + public T jsonRead(SpiJsonReader jsonRead, String path) throws IOException { + return readJsonElement(jsonRead, path); } @SuppressWarnings("unchecked") - T readJsonElement(SpiJsonReader jsonRead, String path, T target) throws IOException { - return (T)targetDescriptor.jsonRead(jsonRead, path, target); + T readJsonElement(SpiJsonReader jsonRead, String path) throws IOException { + return (T)targetDescriptor.jsonRead(jsonRead, path); } void writeJsonElement(SpiJsonWriter ctx, Object element) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java index ecb283e5c2..2021a43cc9 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorElementEmbeddedMap.java @@ -58,13 +58,13 @@ public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) } if (stringKey) { parser.nextToken(); - Object val = readJsonElement(readJson, null, null); // CHECKME: Update existing map entry here? + Object val = readJsonElement(readJson, null); add.addKeyValue(fieldName, val); } else { parser.nextFieldName(); Object key = scalarTypeKey.jsonRead(parser); parser.nextFieldName(); - Object val = readJsonElement(readJson, null, null); // CHECKME: Update existing map entry here? + Object val = readJsonElement(readJson, null); add.addKeyValue(key, val); } } while (true); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java index 720ef92caa..31c5e787b2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorJsonHelp.java @@ -65,7 +65,7 @@ void jsonWriteDirtyProperties(SpiJsonWriter writeJson, EntityBean bean, boolean[ } @SuppressWarnings("unchecked") - T jsonRead(SpiJsonReader jsonRead, String path, boolean withInheritance, T target) throws IOException { + T jsonRead(SpiJsonReader jsonRead, String path, boolean withInheritance) throws IOException { JsonParser parser = jsonRead.parser(); //noinspection StatementWithEmptyBody if (parser.getCurrentToken() == JsonToken.START_OBJECT) { @@ -82,7 +82,7 @@ T jsonRead(SpiJsonReader jsonRead, String path, boolean withInheritance, T targe } if (desc.inheritInfo == null || !withInheritance) { - return jsonReadObject(jsonRead, path, target); + return jsonReadObject(jsonRead, path); } ObjectNode node = jsonRead.mapper().readTree(parser); @@ -97,25 +97,18 @@ T jsonRead(SpiJsonReader jsonRead, String path, boolean withInheritance, T targe JsonNode discNode = node.get(discColumn); if (discNode == null || discNode.isNull()) { if (!desc.isAbstractType()) { - return desc.jsonReadObject(newReader, path, target); + return desc.jsonReadObject(newReader, path); } String msg = "Error reading inheritance discriminator - expected [" + discColumn + "] but no json key?"; throw new JsonParseException(newParser, msg, parser.getCurrentLocation()); } BeanDescriptor inheritDesc = (BeanDescriptor) inheritInfo.readType(discNode.asText()).desc(); - return inheritDesc.jsonReadObject(newReader, path, target); + return inheritDesc.jsonReadObject(newReader, path); } - private T jsonReadObject(SpiJsonReader readJson, String path, T target) throws IOException { - EntityBean bean; - if (target == null) { - bean = desc.createEntityBeanForJson(); - } else if (desc.beanType.isInstance(target)) { - bean = (EntityBean) target; - } else { - throw new ClassCastException(target.getClass().getName() + " provided, but " + desc.beanType.getClass().getName() + " expected"); - } + private T jsonReadObject(SpiJsonReader readJson, String path) throws IOException { + EntityBean bean = desc.createEntityBeanForJson(); return jsonReadProperties(readJson, bean, path); } @@ -133,12 +126,7 @@ private T jsonReadProperties(SpiJsonReader readJson, EntityBean bean, String pat String key = parser.getCurrentName(); BeanProperty p = desc.beanProperty(key); if (p != null) { - if (p.isVersion() && readJson.update() ) { - // skip version prop during update - p.jsonRead(readJson); - } else { - p.jsonRead(readJson, bean); - } + p.jsonRead(readJson, bean); } else { // read an unmapped property if (unmappedProperties == null) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMergeHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMergeHelp.java new file mode 100644 index 0000000000..af187729cc --- /dev/null +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanMergeHelp.java @@ -0,0 +1,192 @@ +package io.ebeaninternal.server.deploy; + +import io.ebean.BeanMergeOptions; +import io.ebean.bean.EntityBean; +import io.ebean.bean.PersistenceContext; +import io.ebeaninternal.server.json.PathStack; +import io.ebeaninternal.server.transaction.DefaultPersistenceContext; + +import java.util.IdentityHashMap; +import java.util.Map; + +import static io.ebeaninternal.server.persist.DmlUtil.isNullOrZero; + +/** + * Helper class to control the merge process of two beans. + *

+ * It holds the persistence context used during merge and various options to fine tune the merge. + * + * @author Roland Praml, FOCONIS AG + */ +class BeanMergeHelp { + + private static final Object DUMMY = new Object(); + private final PersistenceContext persistenceContext; + private final Map processedBeans = new IdentityHashMap<>(); + private final PathStack pathStack; + private final boolean mergeId; + private final boolean mergeVersion; + private final boolean clearCollections; + private final boolean addExistingToPersistenceContext; + private final BeanMergeOptions.MergeHandler mergeHandler; + + + public BeanMergeHelp(EntityBean rootBean, BeanMergeOptions options) { + this.persistenceContext = extractPersistenceContext(rootBean, options); + if (options == null) { + this.mergeHandler = null; + this.pathStack = null; + this.mergeId = true; + this.mergeVersion = false; + this.clearCollections = true; + this.addExistingToPersistenceContext = true; + } else { + this.mergeHandler = options.getMergeHandler(); + this.pathStack = mergeHandler == null ? null : new PathStack(); + this.mergeId = options.isMergeId(); + this.mergeVersion = options.isMergeVersion(); + this.clearCollections = options.isClearCollections(); + this.addExistingToPersistenceContext = true; + } + } + + private PersistenceContext extractPersistenceContext(EntityBean rootBean, BeanMergeOptions options) { + PersistenceContext pc = options == null ? null : options.getPersistenceContext(); + if (pc == null && rootBean != null) { + pc = rootBean._ebean_getIntercept().persistenceContext(); + } + if (pc == null) { + pc = new DefaultPersistenceContext(); + } + return pc; + } + + /** + * Returns, if this bean is already processed. This is to avoid cycles. Internally we use an IdentityHasnMap. + */ + private boolean processed(EntityBean bean) { + return processedBeans.put(bean, DUMMY) != null; + } + + /** + * The persistence context. + */ + public PersistenceContext persistenceContext() { + return persistenceContext; + } + + /** + * Should we add existing beans to the current persistence-context? + */ + public boolean addExistingToPersistenceContext() { + return addExistingToPersistenceContext; + } + + /** + * Add a given bean to the persistence-context. Returns the bean from the PC, + * if it was already there. + */ + public EntityBean contextPutExisting(BeanDescriptor desc, EntityBean bean) { + if (bean == null || !addExistingToPersistenceContext) { + return null; + } + Object id = desc.id(bean); + if (!isNullOrZero(id)) { + return desc.contextPutIfAbsent(persistenceContext, id, bean); + } + return null; + } + + public EntityBean contextGet(BeanDescriptor desc, EntityBean bean) { + if (bean == null) { + return null; + } + Object id = desc.id(bean); + if (!isNullOrZero(id)) { + return (EntityBean) desc.contextGet(persistenceContext, id); + } + return null; + } + + /** + * Checks, if we shold merge that property. + */ + boolean checkMerge(BeanProperty property, EntityBean bean, EntityBean existing) { + if (!bean._ebean_getIntercept().isLoadedProperty(property.propertyIndex())) { + return false; + } + if (property.isId() && !mergeId) { + return false; + } + if (property.isVersion() && !mergeVersion) { + return false; + } + return mergeHandler == null || mergeHandler.mergeBeans(bean, existing, property, pathStack.peekWithNull()); + + } + + public void pushPath(String name) { + if (pathStack != null) { + pathStack.pushPathKey(name); + } + } + + public void popPath() { + if (pathStack != null) { + pathStack.pop(); + } + } + + public boolean clearCollections() { + return clearCollections; + } + + /** + * Merges two beans. Returns the merge result. This is + *

    + *
  • null if bean was null
  • + *
  • context bean if found in context + *
  • existing if existing war not null
  • + *
  • new instance if existing was null
  • + *
+ */ + public EntityBean mergeBeans(BeanDescriptor desc, EntityBean bean, EntityBean existing) { + if (bean == null) { + return null; + } + Object id = desc.id(bean); + if (!isNullOrZero(id)) { + EntityBean contextBean = (EntityBean) desc.contextGet(persistenceContext, id); + if (contextBean != null) { + existing = contextBean; + } + } + if (processed(bean)) { + return existing; + } + + if (desc.inheritInfo() != null) { + desc = desc.inheritInfo().readType(bean.getClass()).desc(); + } + + if (existing == null && !isNullOrZero(id)) { + if (desc.isReference(bean._ebean_getIntercept())) { + existing = (EntityBean) desc.createRef(id, persistenceContext); + } else { + // Now, we must hit the database and try to find the reference bean + existing = (EntityBean) desc.findReferenceBean(id, persistenceContext); + } + } + if (existing == null) { + existing = desc.createEntityBean(); + } + + for (BeanProperty prop : desc.propertiesAll()) { + if (checkMerge(prop, bean, existing)) { + prop.merge(bean, existing, this); + } + } + + return existing; + } +} diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java index 712a27455f..9fa4087c26 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanProperty.java @@ -1440,11 +1440,7 @@ private void jsonWriteScalar(SpiJsonWriter writeJson, Object value) throws IOExc public void jsonRead(SpiJsonReader ctx, EntityBean bean) throws IOException { Object objValue = jsonRead(ctx); if (jsonDeserialize) { - if (ctx.update()) { - setValueIntercept(bean, objValue); - } else { - setValue(bean, objValue); - } + setValue(bean, objValue); } } @@ -1509,8 +1505,16 @@ private boolean isKeywordType(DocPropertyType type, DocPropertyOptions docOption return type == DocPropertyType.TEXT && (docOptions.isCode() || id || discriminator); } - public void merge(EntityBean bean, EntityBean existing) { - // do nothing unless Many property + public void merge(EntityBean bean, EntityBean existing, BeanMergeHelp mergeHelp) { + if (isDiscriminator()) { + return; + } + Object val = getValue(bean); + if (val != null && scalarType != null && scalarType.mutable()) { + // for mutable types, we must "clone" the object. + val = scalarType.parse(scalarType.format(val)); + } + setValueIntercept(existing, val); } public void registerColumn(BeanDescriptor desc, String prefix) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index cc40fed56f..6666ee2182 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -2,8 +2,6 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import io.ebean.SqlUpdate; import io.ebean.Transaction; import io.ebean.bean.BeanCollection; @@ -29,7 +27,6 @@ import java.util.*; import static java.lang.System.Logger.Level.ERROR; -import static java.lang.System.Logger.Level.WARNING; /** * Property mapped to a List Set or Map. @@ -210,22 +207,47 @@ public Collection rawCollection(EntityBean bean) { * Copy collection value if existing is empty. */ @Override - public void merge(EntityBean bean, EntityBean existing) { + public void merge(EntityBean bean, EntityBean existing, BeanMergeHelp mergeHelp) { + mergeHelp.pushPath(name); + Object fromCollection = beanCollection(bean); Object existingCollection = getValue(existing); - if (existingCollection instanceof BeanCollection) { - BeanCollection toBC = (BeanCollection) existingCollection; - if (!toBC.isPopulated()) { - Object fromCollection = getValue(bean); - if (fromCollection instanceof BeanCollection) { - BeanCollection fromBC = (BeanCollection) fromCollection; - if (fromBC.isPopulated()) { - toBC.loadFrom(fromBC); + if (fromCollection instanceof BeanCollection) { + BeanCollection fromBC = (BeanCollection) fromCollection; + + if (fromBC.isPopulated()) { + BeanCollection toBC; + if (existingCollection instanceof BeanCollection) { + toBC = (BeanCollection) existingCollection; + if (mergeHelp.addExistingToPersistenceContext()) { + for (Object detailBean : toBC.actualEntries(true)) { + mergeHelp.contextPutExisting(targetDescriptor, (EntityBean) detailBean); + } + } + if (mergeHelp.clearCollections()) { + toBC.clear(); + } + } else { + toBC = createEmpty(existing); + setValueIntercept(existing, toBC); + } + + for (Object detailBean : fromBC.actualDetails()) { + if (detailBean instanceof EntityBean) { + EntityBean toBean = mergeHelp.mergeBeans(targetDescriptor, (EntityBean) detailBean, null); + if (childMasterProperty != null) { + childMasterProperty.setValue(toBean, existing); + } + toBC.addBean(toBean); } } } + } else { + setValueIntercept(existing, fromCollection); } + mergeHelp.popPath(); } + /** * Add the bean to the appropriate collection on the parent bean. */ @@ -1053,7 +1075,7 @@ private Object jsonReadCollection(String json) throws IOException { if (JsonToken.VALUE_NULL == event) { return null; } - return jsonReadCollection(ctx, null, null); + return jsonReadCollection(ctx, null); } /** @@ -1066,16 +1088,15 @@ private void jsonWriteCollection(SpiJsonWriter ctx, String name, Object value, b /** * Read the collection (JSON Array) containing entity beans. */ - public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean, Object targets) throws IOException { + public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException { if (elementDescriptor != null && elementDescriptor.isJsonReadCollection()) { return elementDescriptor.jsonReadCollection(readJson, parentBean); } BeanCollection collection = createEmpty(parentBean); BeanCollectionAdd add = beanCollectionAdd(collection); - Map existingBeans = extractBeans(targets); do { - EntityBean detailBean = getDetailBean(readJson, existingBeans); + EntityBean detailBean = (EntityBean) targetDescriptor.jsonRead(readJson, name); if (detailBean == null) { // read the entire array break; @@ -1090,63 +1111,6 @@ public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean, return collection; } - /** - * Find bean in the target collection and reuse it for JSON update. - */ - private EntityBean getDetailBean(SpiJsonReader readJson, Map targets) throws IOException { - BeanProperty idProperty = targetDescriptor.idProperty(); - if (targets == null || idProperty == null) { - return (EntityBean) targetDescriptor.jsonRead(readJson, name, null); - } else { - JsonToken token = readJson.parser().nextToken(); - if (JsonToken.VALUE_NULL == token || JsonToken.END_ARRAY == token) { - return null; - } - // extract the id. We have to buffer the JSON; - ObjectNode node = readJson.mapper().readTree(readJson.parser()); - SpiJsonReader jsonReader = readJson.forJson(node.traverse()); - JsonNode idNode = node.get(idProperty.name()); - Object id = idNode == null ? null : idProperty.jsonRead(readJson.forJson(idNode.traverse())); - return (EntityBean) targetDescriptor.jsonRead(jsonReader, name, targets.get(id)); - } - } - - /** - * Extract beans, that are currently in the target collection. (targets can be a List/Set/Map) - */ - private Map extractBeans(Object targets) { - Collection beans; - - if (targets == null) { - return null; - } else if (targets instanceof Map) { - if (((Map) targets).isEmpty()) { - return null; - } - beans = ((Map) targets).values(); - } else if (targets instanceof Collection) { - if (((Collection) targets).isEmpty()) { - return null; - } - beans = (Collection) targets; - } else { - CoreLog.log.log(WARNING, "Found non collection value " + targets.getClass().getSimpleName()); - return null; - } - - BeanProperty idProp = targetDescriptor.idProperty(); - Map ret = new HashMap<>(); - for (T bean : beans) { - if (bean instanceof EntityBean) { - Object id = idProp.getValue((EntityBean) bean); - if (id != null) { - ret.put(id, bean); - } - } - } - return ret.isEmpty() ? null : ret; - } - /** * Bind all the property values to the SqlUpdate. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java index 4bc7307fe0..707256eb2d 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocManyJsonHelp.java @@ -50,11 +50,8 @@ public void jsonRead(SpiJsonReader readJson, EntityBean parentBean) throws IOExc if (JsonToken.START_ARRAY != event && JsonToken.START_OBJECT != event) { throw new JsonParseException(parser, "Unexpected token " + event + " - expecting start array or object"); } - if (readJson.update()) { - many.setValueIntercept(parentBean, many.jsonReadCollection(readJson, parentBean, many.getValue(parentBean))); - } else { - many.setValue(parentBean, many.jsonReadCollection(readJson, parentBean, null)); - } + + many.setValue(parentBean, many.jsonReadCollection(readJson, parentBean)); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java index be53395af0..d03097ff58 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocOne.java @@ -30,10 +30,9 @@ import jakarta.persistence.PersistenceException; import java.io.IOException; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; + +import static io.ebeaninternal.server.persist.DmlUtil.isNullOrZero; /** * Property mapped to a joined bean. @@ -809,22 +808,11 @@ public void jsonWrite(SpiJsonWriter writeJson, EntityBean bean) throws IOExcepti @Override public void jsonRead(SpiJsonReader readJson, EntityBean bean) throws IOException { if (jsonDeserialize && targetDescriptor != null) { - // CHECKME: may we skip reading the object from the json stream? - T target = readJson.update() ? (T) getValue(bean) : null; - T assocBean = targetDescriptor.jsonRead(readJson, name, target); - if (readJson.update()) { - setValueIntercept(bean, assocBean); - } else { - setValue(bean, assocBean); - } + T assocBean = targetDescriptor.jsonRead(readJson, name); + setValue(bean, assocBean); } } - @Override - public Object jsonRead(SpiJsonReader readJson) throws IOException { - return targetDescriptor.jsonRead(readJson, name, null); - } - public boolean isReference(Object detailBean) { EntityBean eb = (EntityBean) detailBean; return targetDescriptor.isReference(eb._ebean_getIntercept()); @@ -847,6 +835,26 @@ public void setParentBeanToChild(EntityBean parent, EntityBean child) { } } + @Override + public void merge(EntityBean bean, EntityBean existing, BeanMergeHelp mergeHelp) { + mergeHelp.pushPath(name); + + EntityBean beanValue = valueAsEntityBean(bean); + EntityBean existingValue = valueAsEntityBean(existing); + + if (beanValue != null && existingValue != null) { + Object beanId = targetDescriptor.id(beanValue); + Object existingId = targetDescriptor.id(existingValue); + if (!isNullOrZero(beanId) && !isNullOrZero(existingId) && !Objects.equals(beanId, existingId)) { + existingValue = null; + } + } + + setValueIntercept(existing, mergeHelp.mergeBeans(targetDescriptor, beanValue, existingValue)); + + mergeHelp.popPath(); + } + public boolean hasCircularImportedId(BeanDescriptor sourceDesc) { return targetDescriptor.hasCircularImportedIdTo(sourceDesc); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java index dc170974d7..2fbf41dcc7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertySimpleCollection.java @@ -19,7 +19,7 @@ public void bindElementValue(SqlUpdate insert, Object value) { } @Override - public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean, Object collectionValue) throws IOException { + public Object jsonReadCollection(SpiJsonReader readJson, EntityBean parentBean) throws IOException { return elementDescriptor.jsonReadCollection(readJson, parentBean); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java index f1b92a78bb..1f7fa08dcf 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonBeanReader.java @@ -36,9 +36,9 @@ public PersistenceContext getPersistenceContext() { } @Override - public T read(T target) { + public T read() { try { - return desc.jsonRead(readJson, null, target); + return desc.jsonRead(readJson, null); } catch (IOException e) { throw new PersistenceIOException(e); } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java index 39b80344b9..ebf3b923a7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/DJsonContext.java @@ -1,21 +1,12 @@ package io.ebeaninternal.server.json; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import io.ebean.FetchPath; import io.ebean.bean.EntityBean; import io.ebean.config.JsonConfig; import io.ebean.plugin.BeanType; -import io.ebean.text.json.EJson; -import io.ebean.text.json.JsonIOException; -import io.ebean.text.json.JsonReadOptions; -import io.ebean.text.json.JsonWriteBeanVisitor; -import io.ebean.text.json.JsonWriteOptions; +import io.ebean.text.json.*; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.SpiJsonContext; import io.ebeaninternal.api.json.SpiJsonReader; @@ -26,19 +17,10 @@ import io.ebeaninternal.util.ParamTypeHelper.ManyType; import io.ebeaninternal.util.ParamTypeHelper.TypeInfo; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.io.StringWriter; -import java.io.Writer; +import java.io.*; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; /** * Default implementation of JsonContext. @@ -124,7 +106,7 @@ public T toBean(Class cls, JsonParser parser) throws JsonIOException { public T toBean(Class cls, JsonParser parser, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = getDescriptor(cls); try { - return desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), false), null, null); + return desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options)), null); } catch (IOException e) { throw new JsonIOException(e); } @@ -136,8 +118,8 @@ public void toBean(T target, String json) throws JsonIOException { } @Override - public void toBean(T target, String json, JsonReadOptions options) throws JsonIOException { - toBean(target, new StringReader(json), options); + public void toBean(T target, String json, JsonReadOptions readOptions) throws JsonIOException { + toBean(target, new StringReader(json), readOptions); } @Override @@ -146,8 +128,8 @@ public void toBean(T target, Reader jsonReader) throws JsonIOException { } @Override - public void toBean(T target, Reader jsonReader, JsonReadOptions options) throws JsonIOException { - toBean(target, createParser(jsonReader), options); + public void toBean(T target, Reader jsonReader, JsonReadOptions readOptions) throws JsonIOException { + toBean(target, createParser(jsonReader), readOptions); } @Override @@ -157,10 +139,12 @@ public void toBean(T target, JsonParser parser) throws JsonIOException { @SuppressWarnings("unchecked") @Override - public void toBean(T target, JsonParser parser, JsonReadOptions options) throws JsonIOException { + @Deprecated + public void toBean(T target, JsonParser parser, JsonReadOptions readOptions) throws JsonIOException { BeanDescriptor desc = (BeanDescriptor) getDescriptor(target.getClass()); try { - desc.jsonRead(new ReadJson(desc, parser, options, determineObjectMapper(options), target != null), null, target); + T bean = desc.jsonRead(new ReadJson(desc, parser, readOptions, determineObjectMapper(readOptions)), null); + desc.mergeBeans((EntityBean) bean, (EntityBean) target, null); } catch (IOException e) { throw new JsonIOException(e); } @@ -169,13 +153,13 @@ public void toBean(T target, JsonParser parser, JsonReadOptions options) thr @Override public DJsonBeanReader createBeanReader(Class cls, JsonParser parser, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = getDescriptor(cls); - return new DJsonBeanReader<>(desc, new ReadJson(desc, parser, options, determineObjectMapper(options), false)); + return new DJsonBeanReader<>(desc, new ReadJson(desc, parser, options, determineObjectMapper(options))); } @Override public DJsonBeanReader createBeanReader(BeanType beanType, JsonParser parser, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = (BeanDescriptor) beanType; - SpiJsonReader readJson = new ReadJson(desc, parser, options, determineObjectMapper(options), false); + SpiJsonReader readJson = new ReadJson(desc, parser, options, determineObjectMapper(options)); return new DJsonBeanReader<>(desc, readJson); } @@ -207,7 +191,7 @@ public List toList(Class cls, JsonParser src) throws JsonIOException { @Override public List toList(Class cls, JsonParser src, JsonReadOptions options) throws JsonIOException { BeanDescriptor desc = getDescriptor(cls); - SpiJsonReader readJson = new ReadJson(desc, src, options, determineObjectMapper(options), false); + SpiJsonReader readJson = new ReadJson(desc, src, options, determineObjectMapper(options)); try { JsonToken currentToken = src.getCurrentToken(); @@ -221,7 +205,7 @@ public List toList(Class cls, JsonParser src, JsonReadOptions options) List list = new ArrayList<>(); do { // CHECKME: Should we update the list - T bean = desc.jsonRead(readJson, null, null); + T bean = desc.jsonRead(readJson, null); if (bean == null) { break; } else { @@ -386,7 +370,7 @@ private void toJsonInternal(Object value, JsonGenerator gen, JsonWriteOptions op public SpiJsonReader createJsonRead(BeanType beanType, String json) { BeanDescriptor desc = (BeanDescriptor) beanType; JsonParser parser = createParser(new StringReader(json)); - return new ReadJson(desc, parser, null, defaultObjectMapper, false); + return new ReadJson(desc, parser, null, defaultObjectMapper); } @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java index ab9100c9f6..d5d637c8f6 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/ReadJson.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.ObjectMapper; +import io.ebean.BeanMergeOptions; import io.ebean.bean.EntityBean; import io.ebean.bean.EntityBeanIntercept; import io.ebean.bean.PersistenceContext; @@ -32,13 +33,12 @@ public final class ReadJson implements SpiJsonReader { private final Object objectMapper; private final PersistenceContext persistenceContext; private final LoadContext loadContext; - private final boolean update; private final boolean enableLazyLoading; /** * Construct with parser and readOptions. */ - public ReadJson(BeanDescriptor desc, JsonParser parser, JsonReadOptions readOptions, Object objectMapper, boolean update) { + public ReadJson(BeanDescriptor desc, JsonParser parser, JsonReadOptions readOptions, Object objectMapper) { this.rootDesc = desc; this.parser = parser; this.objectMapper = objectMapper; @@ -48,7 +48,6 @@ public ReadJson(BeanDescriptor desc, JsonParser parser, JsonReadOptions readO // only create visitorMap, pathStack if needed ... this.visitorMap = (readOptions == null) ? null : readOptions.getVisitorMap(); this.pathStack = (visitorMap == null && loadContext == null) ? null : new PathStack(); - this.update = update; } /** @@ -62,7 +61,6 @@ private ReadJson(JsonParser moreJson, ReadJson source) { this.objectMapper = source.objectMapper; this.persistenceContext = source.persistenceContext; this.loadContext = source.loadContext; - this.update = source.update; this.enableLazyLoading = source.enableLazyLoading; } @@ -119,9 +117,14 @@ public Object persistenceContextPutIfAbsent(Object id, EntityBean bean, BeanDesc // no persistenceContext means no lazy loading either return null; } - Object existing = beanDesc.contextPutIfAbsent(persistenceContext, id, bean); + EntityBean existing = beanDesc.contextPutIfAbsent(persistenceContext, id, bean); if (existing != null) { - beanDesc.merge(bean, (EntityBean) existing); + // we foind a bean in the persistence context AND we have deserialized the same bean + // so copy every property to the existing bean + BeanMergeOptions opts = new BeanMergeOptions(); + opts.setPersistenceContext(persistenceContext); + opts.setMergeVersion(true); + beanDesc.mergeBeans(bean, existing, opts); } else { if (loadContext != null) { EntityBeanIntercept ebi = bean._ebean_getIntercept(); @@ -192,7 +195,7 @@ public void popPath() { * call it's visit method with the bean and unmappedProperties. */ @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({"unchecked", "rawtypes"}) public void beanVisitor(Object bean, Map unmappedProperties) { if (visitorMap != null) { JsonReadBeanVisitor visitor = visitorMap.get(pathStack.peekWithNull()); @@ -211,12 +214,4 @@ public void beanVisitor(Object bean, Map unmappedProperties) { public Object readValueUsingObjectMapper(Class propertyType) throws IOException { return mapper().readValue(parser, propertyType); } - - /** - * Do we update an existing bean? This meeans we have to set values via intercept and handle collections. - */ - @Override - public boolean update() { - return update; - } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java index 449b659261..06e5de5b58 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/STreeType.java @@ -87,7 +87,7 @@ public interface STreeType { /** * Put the entity bean into the persistence context. */ - Object contextPutIfAbsent(PersistenceContext persistenceContext, Object id, EntityBean localBean); + EntityBean contextPutIfAbsent(PersistenceContext persistenceContext, Object id, EntityBean localBean); /** * Set draft status on the entity bean. diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java index 3575d8bbbb..ed6d03fdbd 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/SqlTreeLoadBean.java @@ -184,7 +184,7 @@ private void readId() throws SQLException { private void readIdBean() { // check the PersistenceContext to see if the bean already exists - contextBean = (EntityBean) localDesc.contextPutIfAbsent(persistenceContext, id, localBean); + contextBean = localDesc.contextPutIfAbsent(persistenceContext, id, localBean); if (contextBean == null) { // bean just added to the persistenceContext contextBean = localBean; diff --git a/ebean-core/src/test/java/io/ebeaninternal/server/deploy/BeanDescriptorTest.java b/ebean-core/src/test/java/io/ebeaninternal/server/deploy/BeanDescriptorTest.java index 89f77d70b6..9efddfaace 100644 --- a/ebean-core/src/test/java/io/ebeaninternal/server/deploy/BeanDescriptorTest.java +++ b/ebean-core/src/test/java/io/ebeaninternal/server/deploy/BeanDescriptorTest.java @@ -135,7 +135,7 @@ public void merge_when_empty() { from.setName("rob"); Customer to = new Customer(); - customerDesc.merge((EntityBean) from, (EntityBean) to); + customerDesc.mergeBeans((EntityBean) from, (EntityBean) to, null); assertThat(to.getId()).isEqualTo(42); assertThat(to.getName()).isEqualTo("rob"); diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java index 71160f1525..b3fdd11d25 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiEbeanServer.java @@ -255,6 +255,11 @@ public void merge(Object bean, MergeOptions options) { public void merge(Object bean, MergeOptions options, Transaction transaction) { } + @Override + public T mergeBeans(T bean, T existing, BeanMergeOptions options) { + return null; + } + @Override public List> findVersions(SpiQuery query) { return null; diff --git a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java index a16f2835fd..ebbe715f1e 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java +++ b/ebean-test/src/test/java/io/ebean/xtest/internal/api/TDSpiServer.java @@ -428,6 +428,12 @@ public void merge(Object bean, MergeOptions options, Transaction transaction) { } + @Override + public T mergeBeans(T bean, T existing, BeanMergeOptions options) { + return null; + } + + @Override public void insert(Object bean) { diff --git a/ebean-test/src/test/java/org/tests/model/basic/Customer.java b/ebean-test/src/test/java/org/tests/model/basic/Customer.java index 0eff23d55a..b97895fbf4 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/Customer.java +++ b/ebean-test/src/test/java/org/tests/model/basic/Customer.java @@ -90,7 +90,7 @@ public String getValue() { List orders; @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) - List contacts; + List contacts = new ArrayList<>(); @Override public String toString() { diff --git a/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java b/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java index 96465c4d36..0254716db2 100644 --- a/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java +++ b/ebean-test/src/test/java/org/tests/text/json/TestJsonBeanDescriptorParse.java @@ -1,8 +1,12 @@ package org.tests.text.json; import com.fasterxml.jackson.core.JsonParser; +import io.ebean.BeanMergeOptions; import io.ebean.BeanState; import io.ebean.DB; +import io.ebean.ValuePair; +import io.ebean.test.LoggedSql; +import io.ebean.text.json.JsonReadOptions; import io.ebean.xtest.BaseTestCase; import io.ebeaninternal.api.SpiEbeanServer; import io.ebeaninternal.api.json.SpiJsonReader; @@ -17,12 +21,12 @@ import java.io.IOException; import java.io.StringReader; import java.util.Comparator; +import java.util.List; +import java.util.Map; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; public class TestJsonBeanDescriptorParse extends BaseTestCase { @@ -37,6 +41,7 @@ void setup() { customer.setName("Hello Roland"); customer.setId(234); Address address = new Address(); + address.setId(234); address.setLine1("foo"); DB.save(address); customer.setBillingAddress(address); @@ -57,6 +62,7 @@ void setup() { @AfterEach void teardown() { DB.delete(Customer.class, 234); + DB.delete(UTMaster.class, 1); } @Test @@ -65,7 +71,7 @@ public void testJsonRead() throws IOException { BeanDescriptor descriptor = server.descriptor(Customer.class); SpiJsonReader readJson = createRead(server, descriptor); - Customer customer = descriptor.jsonRead(readJson, null, null); + Customer customer = descriptor.jsonRead(readJson, null); assertEquals(Integer.valueOf(123), customer.getId()); assertEquals("Hello Rob", customer.getName()); @@ -90,7 +96,15 @@ public void testJsonManyUpdate() { + " ]}," + " {\"id\": 790, \"firstName\": \"Anton\", \"lastName\": null, \"notes\" : null } " + "]}"; - DB.json().toBean(customer, json); + + BeanMergeOptions opts = new BeanMergeOptions(); + // example for a merge handler + // opts.setMergeHandler((a, b, c, d) -> { + // System.out.println("Merge " + d + "." +c); + // return true; + // }); + Customer jsonBean = DB.json().toBean(Customer.class, json); + DB.getDefault().mergeBeans(jsonBean, customer, null); DB.save(customer); customer = DB.find(Customer.class, 234); @@ -128,9 +142,15 @@ public void testJsonUpdate() { assertEquals(null, customer.getName()); assertEquals("foo", customer.getBillingAddress().getLine1()); - DB.json().toBean(customer, "{\"billingAddress\":{\"line1\":\"foo\"}}"); + JsonReadOptions opts = new JsonReadOptions(); + opts.setEnableLazyLoading(true); + + Customer jsonBean = DB.json().toBean(Customer.class, "{\"billingAddress\":{\"line1\":\"foo\"}}", opts); + DB.getDefault().mergeBeans(jsonBean, customer, null); assertFalse(DB.beanState(customer.getBillingAddress()).isDirty()); - DB.json().toBean(customer, "{\"billingAddress\":{\"line1\":\"bar\"}}"); + + jsonBean = DB.json().toBean(Customer.class, "{\"billingAddress\":{\"line1\":\"bar\"}}", opts); + DB.getDefault().mergeBeans(jsonBean, customer, null); assertEquals("bar", customer.getBillingAddress().getLine1()); assertTrue(DB.beanState(customer.getBillingAddress()).isDirty()); @@ -183,11 +203,90 @@ public void testJsonUpdate() { DB.delete(customer); // cleanup*/ } + @Test + public void testJsonLazyRead() throws IOException { + JsonReadOptions opts = new JsonReadOptions(); + opts.setEnableLazyLoading(true); + Customer customer = DB.json().toBean(Customer.class, "{\"id\": 234}", opts); + assertThat(customer.getBillingAddress().getLine1()).isEqualTo("foo"); + + } + + @Test + public void testJsonCollectionUnpopulated() throws IOException { + + Customer customer = DB.json().toBean(Customer.class, "{\"id\": 234, \"name\" : \"Roland\"}"); + + assertThat(customer.getBillingAddress()).isNull(); + List contacts = customer.getContacts(); + + assertThat(contacts).isEmpty(); + ; + + } + + @Test + public void testJsonUpdateManyToOne() throws IOException { + Customer customer = DB.find(Customer.class, 234); + assertThat(customer.getBillingAddress().getLine1()).isEqualTo("foo"); + + Address address = new Address(); + address.setId(987); + address.setLine1("bar"); + DB.save(address); + customer.setBillingAddress(address); + DB.save(customer); + + assertThat(customer.getBillingAddress().getLine1()).isEqualTo("bar"); + JsonReadOptions opts = new JsonReadOptions(); + + LoggedSql.start(); + Customer jsonBean = DB.json().toBean(Customer.class, "{\"billingAddress\":{\"id\": 234, \"line1\" : \"bar\"}}", opts); + DB.getDefault().mergeBeans(jsonBean, customer, null); + assertThat(LoggedSql.stop()).isEmpty(); + + Map dirty = DB.beanState(customer.getBillingAddress()).dirtyValues(); + assertThat(dirty).containsKeys("line1").hasSize(1); + assertThat(customer.getBillingAddress().getLine1()).isEqualTo("bar"); + assertThat(dirty.get("line1").getOldValue()).isEqualTo("foo"); + + } + + @Test + public void testJsonUpdateWithDbJson() { + UTMaster master = new UTMaster("m0"); + master.setId(1); + master.setJournal(new UTMaster.Journal()); + master.getJournal().addEntry(); + master.getJournal().addEntry(); + DB.save(master); + + DB.json().toBean(master, "{\"id\":1,\"name\":\"newName\",\"description\":\"master\",\"journal\":{\"entries\":[\"newEntry\"]},\"details\":[],\"version\":1}"); + + assertThat(master.getName()).isEqualTo("newName"); + assertThat(master.getDescription()).isEqualTo("master"); + assertThat(master.getJournal().getEntries()).hasSize(1); + + DB.json().toBean(master, "{\"id\":1,\"name\":\"name\",\"journal\":{}}"); + + assertThat(master.getName()).isEqualTo("name"); + assertThat(master.getDescription()).isEqualTo("master"); + assertThat(master.getJournal().getEntries()).hasSize(0); + + DB.json().toBean(master, "{\"id\":1,\"name\":\"newName\",\"description\":\"master\",\"journal\":{\"entries\":[\"newEntry\"]},\"details\":[],\"version\":1}"); + + assertThat(master.getName()).isEqualTo("newName"); + assertThat(master.getDescription()).isEqualTo("master"); + assertThat(master.getJournal().getEntries()).hasSize(1); + + + } + private SpiJsonReader createRead(SpiEbeanServer server, BeanDescriptor descriptor) { StringReader reader = new StringReader("{\"id\":123,\"name\":\"Hello Rob\"}"); JsonParser parser = server.json().createParser(reader); - return new ReadJson(descriptor, parser, null, null, false); + return new ReadJson(descriptor, parser, null, null); } } diff --git a/ebean-test/src/test/java/org/tests/text/json/TestTextJsonInheritance.java b/ebean-test/src/test/java/org/tests/text/json/TestTextJsonInheritance.java index 6f2fc22f1a..da1e90f32f 100644 --- a/ebean-test/src/test/java/org/tests/text/json/TestTextJsonInheritance.java +++ b/ebean-test/src/test/java/org/tests/text/json/TestTextJsonInheritance.java @@ -1,12 +1,20 @@ package org.tests.text.json; -import io.ebean.xtest.BaseTestCase; +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.ObjectMapper; import io.ebean.DB; import io.ebean.text.json.JsonContext; +import io.ebean.xtest.BaseTestCase; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.tests.model.basic.*; +import java.io.*; +import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -40,6 +48,73 @@ public void test() { assertEquals(2, rebuiltList.size()); } + @Test + @Disabled("Run manually") + public void testPerformance() throws IOException { + for (int i = 0; i < 100000; i++) { + Truck c = new Truck(); + c.setLicenseNumber("L " + i); + c.setCapacity(20D); + DB.save(c); + } + DB.update(Truck.class).set("capacity", 40D).update(); + List list = DB.find(Vehicle.class).findList(); + assertThat(list).hasSize(100000); + JsonContext jsonContext = DB.json(); + JsonFactory jsonFactory = new JsonFactory(); + for (int i = 0; i < 1; i++) { + long start = System.nanoTime(); + try (OutputStream out = new GZIPOutputStream(new FileOutputStream("temp.json.gz")); + JsonGenerator gen = jsonFactory.createGenerator(out, JsonEncoding.UTF8)) { + jsonContext.toJson(list, gen); + start = System.nanoTime() - start; + System.out.println("Serializing " + 100_000_000_000_000L / start + " Entities/s"); + } + } + + ObjectMapper mapper = new ObjectMapper(); + for (int i = 0; i < 10; i++) { + long start = System.nanoTime(); + try (InputStream in = new GZIPInputStream(new FileInputStream("temp.json.gz")); + JsonParser parser = mapper.createParser(in)) { + parser.nextToken(); + Vehicle v; + List batch = new ArrayList<>(); + List batchId = new ArrayList<>(); + while ((v = read(parser)) != null) { + batchId.add(DB.beanId(v)); + batch.add(v); + if (batch.size() == 100) { + Map vDb = DB.find(Vehicle.class).where().idIn(batchId).findMap(); + for (Vehicle vehicle : batch) { + Vehicle db = vDb.get(vehicle.getId()); + DB.getDefault().pluginApi().mergeBeans(vehicle, db, null); + } + DB.saveAll(vDb.values()); + batch.clear(); + batchId.clear(); + } + } + //jsonContext.toList(Vehicle.class, parser); + start = System.nanoTime() - start; + System.out.println("DeSerializing " + 100_000_000_000_000L / start + " Entities/s"); + } + } + } + + private Vehicle read(JsonParser parser) throws IOException { + JsonToken token = parser.currentToken(); + if (token == null || token == JsonToken.START_ARRAY || token == JsonToken.END_OBJECT) { + // first invocation + token = parser.nextToken(); + } + if (token == JsonToken.START_OBJECT) { + Vehicle ret = DB.json().toBean(Vehicle.class, parser); + return ret; + } + return null; + } + private void setupData() { DB.createUpdate(CarAccessory.class, "delete from CarAccessory").execute(); DB.createUpdate(CarFuse.class, "delete from CarFuse").execute(); From f926a7ace6c50619041688d70c126845b7c2a603 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 12:02:58 +0200 Subject: [PATCH 23/36] NOPR - NEW: QueryCache support for DTO queries --- .../java/io/ebeaninternal/api/HashQuery.java | 7 ++- .../java/io/ebeaninternal/api/SpiQuery.java | 5 ++ .../server/core/AbstractSqlQueryRequest.java | 2 +- .../server/core/DefaultServer.java | 35 ++++++++---- .../server/core/DtoQueryRequest.java | 38 ++++++++----- .../query/DefaultRelationalQueryEngine.java | 15 +++-- .../server/query/DtoQueryEngine.java | 11 ++-- .../server/querydefn/DefaultOrmQuery.java | 8 ++- .../ebean/xtest/base/DtoQueryFromOrmTest.java | 57 ++++++++++++++++++- .../java/org/tests/model/basic/Contact.java | 2 +- 10 files changed, 134 insertions(+), 46 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java index a207f3c573..9f77331016 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/HashQuery.java @@ -7,13 +7,15 @@ public final class HashQuery { private final CQueryPlanKey planHash; private final BindValuesKey bindValuesKey; + private final Class dtoType; /** * Create the HashQuery. */ - public HashQuery(CQueryPlanKey planHash, BindValuesKey bindValuesKey) { + public HashQuery(CQueryPlanKey planHash, BindValuesKey bindValuesKey, Class dtoType) { this.planHash = planHash; this.bindValuesKey = bindValuesKey; + this.dtoType = dtoType; } @Override @@ -25,6 +27,7 @@ public String toString() { public int hashCode() { int hc = 92821 * planHash.hashCode(); hc = 92821 * hc + bindValuesKey.hashCode(); + hc = 92821 * hc + dtoType.hashCode(); return hc; } @@ -37,6 +40,6 @@ public boolean equals(Object obj) { return false; } HashQuery e = (HashQuery) obj; - return e.bindValuesKey.equals(bindValuesKey) && e.planHash.equals(planHash); + return e.bindValuesKey.equals(bindValuesKey) && e.planHash.equals(planHash) && e.dtoType.equals(dtoType); } } diff --git a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java index 3d1e460865..09a69dc89a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/api/SpiQuery.java @@ -877,6 +877,11 @@ public static TemporalMode of(SpiQuery query) { */ void setManualId(); + /** + * Set the DTO type, that should be part of the queryHash. + */ + void setDtoType(Class dtoType); + /** * Set default select clauses where none have been explicitly defined. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java index 4acd78ee93..073a21b4f0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/AbstractSqlQueryRequest.java @@ -126,7 +126,7 @@ private String limitOffset(String sql) { /** * Prepare and execute the SQL using the Binder. */ - public void executeSql(Binder binder, SpiQuery.Type type) throws SQLException { + public void executeSql(Binder binder) throws SQLException { startNano = System.nanoTime(); executeAsSql(binder); } 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 327ff1ef5a..d7f2779183 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 @@ -898,6 +898,7 @@ public DtoQuery createNamedDtoQuery(Class dtoType, String namedQuery) @Override public DtoQuery findDto(Class dtoType, SpiQuery ormQuery) { DtoBeanDescriptor descriptor = dtoBeanManager.descriptor(dtoType); + ormQuery.setDtoType(dtoType); return new DefaultDtoQuery<>(this, descriptor, ormQuery); } @@ -940,6 +941,18 @@ public T find(Class beanType, Object id, @Nullable Transaction transactio return findId(query); } + DtoQueryRequest createDtoQueryRequest(Type type, SpiDtoQuery query) { + SpiQuery ormQuery = query.ormQuery(); + if (ormQuery != null) { + ormQuery.setType(type); + ormQuery.setManualId(); + SpiOrmQueryRequest ormRequest = createQueryRequest(type, ormQuery); + return new DtoQueryRequest<>(this, dtoQueryEngine, query, ormRequest); + } else { + return new DtoQueryRequest<>(this, dtoQueryEngine, query, null); + } + } + SpiOrmQueryRequest createQueryRequest(Type type, SpiQuery query) { SpiOrmQueryRequest request = buildQueryRequest(type, query); request.prepareQuery(); @@ -1544,7 +1557,7 @@ public T findSingleAttribute(SpiSqlQuery query, Class cls) { @Override public void findDtoEach(SpiDtoQuery query, Consumer consumer) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query); try { request.initTransIfRequired(); request.findEach(consumer); @@ -1555,7 +1568,7 @@ public void findDtoEach(SpiDtoQuery query, Consumer consumer) { @Override public void findDtoEach(SpiDtoQuery query, int batch, Consumer> consumer) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query); try { request.initTransIfRequired(); request.findEach(batch, consumer); @@ -1566,7 +1579,7 @@ public void findDtoEach(SpiDtoQuery query, int batch, Consumer> c @Override public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query); try { request.initTransIfRequired(); request.findEachWhile(consumer); @@ -1577,7 +1590,7 @@ public void findDtoEachWhile(SpiDtoQuery query, Predicate consumer) { @Override public QueryIterator findDtoIterate(SpiDtoQuery query) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + DtoQueryRequest request = createDtoQueryRequest(Type.ITERATE, query); try { request.initTransIfRequired(); return request.findIterate(); @@ -1594,7 +1607,11 @@ public Stream findDtoStream(SpiDtoQuery query) { @Override public List findDtoList(SpiDtoQuery query) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); + DtoQueryRequest request = createDtoQueryRequest(Type.LIST, query); + List ret = request.getFromQueryCache(); + if (ret != null) { + return ret; + } try { request.initTransIfRequired(); return request.findList(); @@ -1606,13 +1623,7 @@ public List findDtoList(SpiDtoQuery query) { @Nullable @Override public T findDtoOne(SpiDtoQuery query) { - DtoQueryRequest request = new DtoQueryRequest<>(this, dtoQueryEngine, query); - try { - request.initTransIfRequired(); - return extractUnique(request.findList()); - } finally { - request.endTransIfRequired(); - } + return extractUnique(findDtoList(query)); } /** diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java index 3cb56ec27c..90dafe1d16 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/core/DtoQueryRequest.java @@ -4,7 +4,6 @@ import io.ebean.core.type.DataReader; import io.ebeaninternal.api.SpiDtoQuery; import io.ebeaninternal.api.SpiEbeanServer; -import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.dto.DtoColumn; import io.ebeaninternal.server.dto.DtoMappingRequest; import io.ebeaninternal.server.dto.DtoQueryPlan; @@ -31,11 +30,13 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest { private final DtoQueryEngine queryEngine; private DtoQueryPlan plan; private DataReader dataReader; + private SpiOrmQueryRequest ormRequest; - DtoQueryRequest(SpiEbeanServer server, DtoQueryEngine engine, SpiDtoQuery query) { + DtoQueryRequest(SpiEbeanServer server, DtoQueryEngine engine, SpiDtoQuery query, SpiOrmQueryRequest ormRequest) { super(server, query); this.queryEngine = engine; this.query = query; + this.ormRequest = ormRequest; query.obtainLocation(); } @@ -43,21 +44,16 @@ public final class DtoQueryRequest extends AbstractSqlQueryRequest { * Prepare and execute the SQL using the Binder. */ @Override - public void executeSql(Binder binder, SpiQuery.Type type) throws SQLException { + public void executeSql(Binder binder) throws SQLException { startNano = System.nanoTime(); - SpiQuery ormQuery = query.ormQuery(); - if (ormQuery != null) { - ormQuery.setType(type); - ormQuery.setManualId(); - - query.setCancelableQuery(ormQuery); + if (ormRequest != null) { // execute the underlying ORM query returning the ResultSet - ormQuery.usingTransaction(transaction); - SpiResultSet result = server.findResultSet(ormQuery); + query.setCancelableQuery(query.ormQuery()); + ormRequest.transaction(transaction); + SpiResultSet result = ormRequest.findResultSet(); this.pstmt = result.statement(); - this.sql = ormQuery.getGeneratedSql(); - setResultSet(result.resultSet(), ormQuery.queryPlanKey()); - + this.sql = ormRequest.query().getGeneratedSql(); + setResultSet(result.resultSet(), ormRequest.query().queryPlanKey()); } else { // native SQL query execution executeAsSql(binder); @@ -156,4 +152,18 @@ static String parseColumn(String columnLabel) { return columnLabel; } + public List getFromQueryCache() { + if (ormRequest != null) { + return ormRequest.getFromQueryCache(); + } else { + return null; + } + } + + public void putToQueryCache(List result) { + if (ormRequest != null && ormRequest.isQueryCachePut()) { + ormRequest.putToQueryCache(result); + } + } + } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java index 3d5dab14e1..6f3e013653 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DefaultRelationalQueryEngine.java @@ -8,7 +8,6 @@ import io.ebean.meta.MetricVisitor; import io.ebean.metric.MetricFactory; import io.ebean.metric.TimedMetricMap; -import io.ebeaninternal.api.SpiQuery; import io.ebeaninternal.server.core.RelationalQueryEngine; import io.ebeaninternal.server.core.RelationalQueryRequest; import io.ebeaninternal.server.core.RowReader; @@ -59,7 +58,7 @@ private String errMsg(String msg, String sql) { @Override public void findEach(RelationalQueryRequest request, RowConsumer consumer) { try { - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); request.mapEach(consumer); request.logSummary(); @@ -74,7 +73,7 @@ public void findEach(RelationalQueryRequest request, RowConsumer consumer) { @Override public void findEach(RelationalQueryRequest request, RowReader reader, Predicate consumer) { try { - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); while (request.next()) { if (!consumer.test(reader.read())) { break; @@ -93,7 +92,7 @@ public void findEach(RelationalQueryRequest request, RowReader reader, Pr @Override public T findOne(RelationalQueryRequest request, RowMapper mapper) { try { - request.executeSql(binder, SpiQuery.Type.BEAN); + request.executeSql(binder); T value = request.mapOne(mapper); request.logSummary(); return value; @@ -109,7 +108,7 @@ public T findOne(RelationalQueryRequest request, RowMapper mapper) { @Override public List findList(RelationalQueryRequest request, RowReader reader) { try { - request.executeSql(binder, SpiQuery.Type.LIST); + request.executeSql(binder); List rows = new ArrayList<>(); while (request.next()) { rows.add(reader.read()); @@ -129,7 +128,7 @@ public List findList(RelationalQueryRequest request, RowReader reader) public T findSingleAttribute(RelationalQueryRequest request, Class cls) { ScalarType scalarType = (ScalarType) binder.getScalarType(cls); try { - request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); + request.executeSql(binder); final DataReader dataReader = binder.createDataReader(request.resultSet()); T value = null; if (dataReader.next()) { @@ -151,7 +150,7 @@ public T findSingleAttribute(RelationalQueryRequest request, Class cls) { public List findSingleAttributeList(RelationalQueryRequest request, Class cls) { ScalarType scalarType = (ScalarType) binder.getScalarType(cls); try { - request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); + request.executeSql(binder); final DataReader dataReader = binder.createDataReader(request.resultSet()); List rows = new ArrayList<>(); while (dataReader.next()) { @@ -173,7 +172,7 @@ public List findSingleAttributeList(RelationalQueryRequest request, Class public void findSingleAttributeEach(RelationalQueryRequest request, Class cls, Consumer consumer) { ScalarType scalarType = (ScalarType) binder.getScalarType(cls); try { - request.executeSql(binder, SpiQuery.Type.ATTRIBUTE); + request.executeSql(binder); final DataReader dataReader = binder.createDataReader(request.resultSet()); while (dataReader.next()) { consumer.accept(scalarType.read(dataReader)); diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java b/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java index d21239444c..1a3784b270 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/query/DtoQueryEngine.java @@ -22,11 +22,12 @@ public DtoQueryEngine(Binder binder) { public List findList(DtoQueryRequest request) { try { - request.executeSql(binder, SpiQuery.Type.LIST); + request.executeSql(binder); List rows = new ArrayList<>(); while (request.next()) { rows.add(request.readNextBean()); } + request.putToQueryCache(rows); return rows; } catch (SQLException e) { @@ -38,7 +39,7 @@ public List findList(DtoQueryRequest request) { public QueryIterator findIterate(DtoQueryRequest request) { try { - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); return new DtoQueryIterator<>(request); } catch (SQLException e) { throw new PersistenceException(errMsg(e.getMessage(), request.getSql()), e); @@ -47,7 +48,7 @@ public QueryIterator findIterate(DtoQueryRequest request) { public void findEach(DtoQueryRequest request, Consumer consumer) { try { - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); while (request.next()) { consumer.accept(request.readNextBean()); } @@ -61,7 +62,7 @@ public void findEach(DtoQueryRequest request, Consumer consumer) { public void findEach(DtoQueryRequest request, int batchSize, Consumer> consumer) { try { List buffer = new ArrayList<>(); - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); while (request.next()) { buffer.add(request.readNextBean()); if (buffer.size() >= batchSize) { @@ -82,7 +83,7 @@ public void findEach(DtoQueryRequest request, int batchSize, Consumer void findEachWhile(DtoQueryRequest request, Predicate consumer) { try { - request.executeSql(binder, SpiQuery.Type.ITERATE); + request.executeSql(binder); while (request.next()) { if (!consumer.test(request.readNextBean())) { break; diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java index 0a433669a1..31521c1c19 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/querydefn/DefaultOrmQuery.java @@ -163,6 +163,7 @@ public class DefaultOrmQuery extends AbstractQuery implements SpiQuery { private String nativeSql; private boolean orderById; private ProfileLocation profileLocation; + private Class dtoType = Object.class; // default: Object saves some null-checks. public DefaultOrmQuery(BeanDescriptor desc, SpiEbeanServer server, ExpressionFactory expressionFactory) { this.beanDescriptor = desc; @@ -1240,7 +1241,7 @@ public final HashQuery queryHash() { // so queryPlanHash is calculated well before this method is called BindValuesKey bindKey = new BindValuesKey(); queryBindKey(bindKey); - return new HashQuery(queryPlanKey, bindKey); + return new HashQuery(queryPlanKey, bindKey, dtoType); } @Override @@ -1726,6 +1727,11 @@ public final void setManualId() { } } + @Override + public void setDtoType(Class dtoType) { + this.dtoType = dtoType; + } + /** * return true if user specified to use SQL DISTINCT (effectively excludes id property). */ diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java b/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java index 503f561eef..4f262911d3 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java +++ b/ebean-test/src/test/java/io/ebean/xtest/base/DtoQueryFromOrmTest.java @@ -1,15 +1,15 @@ package io.ebean.xtest.base; -import io.ebean.xtest.BaseTestCase; import io.ebean.DB; import io.ebean.DtoQuery; import io.ebean.ProfileLocation; -import io.ebean.xtest.ForPlatform; import io.ebean.annotation.Platform; import io.ebean.meta.MetaQueryMetric; import io.ebean.meta.MetaTimedMetric; import io.ebean.meta.ServerMetrics; import io.ebean.test.LoggedSql; +import io.ebean.xtest.BaseTestCase; +import io.ebean.xtest.ForPlatform; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -383,6 +383,37 @@ public void toDto_fromExpressionList() { assertThat(contactDtos).isNotEmpty(); } + @Test + public void toDto_withQueryCache() { + + ResetBasicData.reset(); + LoggedSql.start(); + List contactDtos1 = DB.find(Contact.class) + .setUseQueryCache(true) + .select("lastName, count(*) as totalCount").where() + .isNotNull("lastName").asDto(ContactTotals.class).findList(); + assertThat(LoggedSql.stop()).hasSize(1); + + + LoggedSql.start(); + List contactDtos2 = DB.find(Contact.class) + .setUseQueryCache(true) + .select("lastName, count(*) as totalCount").where() + .isNotNull("lastName").asDto(ContactTotals.class).findList(); + assertThat(LoggedSql.stop()).isEmpty(); + assertThat(contactDtos1).isNotEmpty().isSameAs(contactDtos2); + assertThat(contactDtos1.get(0)).isInstanceOf(ContactTotals.class); + + List contactDtos3 = DB.find(Contact.class) + .setUseQueryCache(true) + .select("lastName, count(*) as totalCount").where() + .isNotNull("lastName").asDto(ContactTotalsInt.class).findList(); + + assertThat(contactDtos3).isNotEmpty(); + assertThat(contactDtos3.get(0)).isInstanceOf(ContactTotalsInt.class); + + } + public static class ContactTotals { String lastName; @@ -410,6 +441,28 @@ public void setTotalCount(Long totalCount) { } } + public static class ContactTotalsInt { + + String lastName; + int totalCount; + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public int getTotalCount() { + return totalCount; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } + } + public static class ContactDto2 { int id; diff --git a/ebean-test/src/test/java/org/tests/model/basic/Contact.java b/ebean-test/src/test/java/org/tests/model/basic/Contact.java index 97a48bf101..5b2b05139e 100644 --- a/ebean-test/src/test/java/org/tests/model/basic/Contact.java +++ b/ebean-test/src/test/java/org/tests/model/basic/Contact.java @@ -12,7 +12,7 @@ @Index(columnNames = {"last_name", "first_name"}) @ChangeLog @Entity -@Cache(naturalKey = "email") +@Cache(naturalKey = "email", enableQueryCache = true) public class Contact { @Id @GeneratedValue From 5b14b13219e18c3fa986bc9a0f20249d85689396 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 11:50:59 +0200 Subject: [PATCH 24/36] PR #3153 - TEST: TestServerOffline --- .../ebean/test/config/TestServerOffline.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 ebean-test/src/test/java/io/ebean/test/config/TestServerOffline.java diff --git a/ebean-test/src/test/java/io/ebean/test/config/TestServerOffline.java b/ebean-test/src/test/java/io/ebean/test/config/TestServerOffline.java new file mode 100644 index 0000000000..228c55e8bb --- /dev/null +++ b/ebean-test/src/test/java/io/ebean/test/config/TestServerOffline.java @@ -0,0 +1,151 @@ +package io.ebean.test.config; + + +import io.ebean.Database; +import io.ebean.DatabaseFactory; +import io.ebean.annotation.Platform; +import io.ebean.config.DatabaseConfig; +import io.ebean.datasource.DataSourceAlert; +import io.ebean.datasource.DataSourceInitialiseException; +import io.ebean.xtest.ForPlatform; + +import io.ebean.xtest.base.PlatformCondition; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.tests.model.basic.EBasicVer; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +import jakarta.persistence.PersistenceException; +import javax.sql.DataSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(PlatformCondition.class) +public class TestServerOffline { + + @Test + @ForPlatform({Platform.H2}) + public void testOffline_default() throws SQLException { + + String url = "jdbc:h2:mem:testoffline1"; + try (Connection bootup = DriverManager.getConnection(url, "sa", "secret")) { + Properties props = props(url); + DatabaseConfig config = config(props); + + assertThatThrownBy(() -> DatabaseFactory.create(config)) + .isInstanceOf(DataSourceInitialiseException.class); + } + + } + + private static class LazyDatasourceInitializer implements DataSourceAlert { + + public Database server; + + private boolean initialized; + + @Override + public void dataSourceUp(DataSource dataSource) { + if (!initialized) { + initDatabase(); + } + } + + public synchronized void initDatabase() { + if (!initialized) { + server.runDdl(); + initialized = true; + } + } + + @Override + public void dataSourceDown(DataSource dataSource, SQLException reason) {} + + } + + @Test + @ForPlatform({Platform.H2}) + public void testOffline_recovery() throws SQLException { + + String url = "jdbc:h2:mem:testoffline3"; + try (Connection bootup = DriverManager.getConnection(url, "sa", "secret")) { + + Properties props = props(url); + + // to bring up ebean without a database, we must disable various things + // that happen on startup + props.setProperty("datasource.h2_offline.failOnStart", "false"); + props.setProperty("ebean.h2_offline.skipDataSourceCheck", "true"); + props.setProperty("ebean.h2_offline.ddl.run", "false"); + DatabaseConfig config = config(props); + + LazyDatasourceInitializer alert = new LazyDatasourceInitializer() ; + config.getDataSourceConfig().alert(alert); + config.getDataSourceConfig().heartbeatFreqSecs(1); + + Database h2Offline = DatabaseFactory.create(config); + alert.server = h2Offline; + assertThat(h2Offline).isNotNull(); + // DB is online now in offline mode + + // Accessing the DB will throw a PE + assertThatThrownBy(() -> alert.initDatabase()) + .isInstanceOf(PersistenceException.class) + .hasMessageContaining("Failed to obtain connection to run DDL"); + + assertThatThrownBy(() -> h2Offline.find(EBasicVer.class).findCount()).isInstanceOf(PersistenceException.class); + + // so - reset the password so that the server can reconnect + try (Statement stmt = bootup.createStatement()) { + stmt.execute("alter user sa set password 'sa'"); + } + + assertThat(alert.initialized).isFalse(); + + // next access to ebean should bring DS online + h2Offline.find(EBasicVer.class).findCount(); + assertThat(alert.initialized).isTrue(); + + // check if server is working (ie ddl was run) + EBasicVer bean = new EBasicVer("foo"); + h2Offline.save(bean); + assertThat(h2Offline.find(EBasicVer.class).findCount()).isEqualTo(1); + h2Offline.delete(bean); + } + } + + private Properties props(String url) { + + Properties props = new Properties(); + + props.setProperty("datasource.h2_offline.username", "sa"); + props.setProperty("datasource.h2_offline.password", "sa"); + props.setProperty("datasource.h2_offline.url", url); + props.setProperty("datasource.h2_offline.driver", "org.h2.Driver"); + + props.setProperty("ebean.h2_offline.databasePlatformName", "h2"); + props.setProperty("ebean.h2_offline.ddl.extra", "false"); + + props.setProperty("ebean.h2_offline.ddl.generate", "true"); + props.setProperty("ebean.h2_offline.ddl.run", "true"); + + return props; + } + + private DatabaseConfig config(Properties props) { + DatabaseConfig config = new DatabaseConfig(); + config.setName("h2_offline"); + config.loadFromProperties(props); + config.setDefaultServer(false); + config.setRegister(false); + config.classes().add(EBasicVer.class); + return config; + } + +} From 0a3fbcd28fc77ce176273253ae7fe7a08ed3d1f1 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 16:11:59 +0200 Subject: [PATCH 25/36] NOPR - DEPRECATED: WriteJson allow include identifier --- .../src/main/java/io/ebeaninternal/server/json/WriteJson.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java b/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java index f5b583822c..f65119cf67 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/json/WriteJson.java @@ -513,6 +513,9 @@ private boolean isIncludeProperty(BeanProperty prop) { return true; if (currentIncludeProps != null) { // explicitly controlled by pathProperties + if (prop.isId() && currentIncludeProps.contains("${identifier}")) { + return true; + } return currentIncludeProps.contains(prop.name()); } else if (includeLoadedImplicit){ // include only loaded properties From ce80ddd9d550cd79995da2fa7d4d7fdcc5d65bc9 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Thu, 10 Aug 2023 12:09:27 +0200 Subject: [PATCH 26/36] NOPR - additional test for Multi-Tenant-With-Master --- .../EbeanServerFactory_MultiTenancy_Test.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java b/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java index 3b5c54f35f..bd2811ab61 100644 --- a/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java +++ b/ebean-test/src/test/java/io/ebean/xtest/base/EbeanServerFactory_MultiTenancy_Test.java @@ -50,6 +50,39 @@ public void create_new_server_with_multi_tenancy_db() { + /** + * Tests using multi tenancy per schema + */ + @Test + public void create_new_server_with_multi_tenancy_db_with_master() { + + String tenant = "customer"; + CurrentTenantProvider tenantProvider = Mockito.mock(CurrentTenantProvider.class); + Mockito.doReturn(tenant).when(tenantProvider).currentId(); + + TenantDataSourceProvider dataSourceProvider = Mockito.mock(TenantDataSourceProvider.class); + + DatabaseConfig config = new DatabaseConfig(); + + config.setName("h2"); + config.loadFromProperties(); + config.setRegister(false); + config.setDefaultServer(false); + config.setDdlGenerate(false); + config.setDdlRun(false); + + config.setTenantMode(TenantMode.DB_WITH_MASTER); + config.setCurrentTenantProvider(tenantProvider); + config.setTenantDataSourceProvider(dataSourceProvider); + + Mockito.doReturn(config.getDataSource()).when(dataSourceProvider).dataSource(tenant); + + config.setDatabasePlatform(new PostgresPlatform()); + + final Database database = DatabaseFactory.create(config); + database.shutdown(); + } + /** * Tests using multi tenancy per schema */ From 588d6634d92d09dc9e7837eb7bb467e1fc04e9be Mon Sep 17 00:00:00 2001 From: Noemi Praml Date: Wed, 9 Oct 2024 16:09:19 +0200 Subject: [PATCH 27/36] ebean-migration FOC Version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f8f1106c49..7aefb0d734 100644 --- a/pom.xml +++ b/pom.xml @@ -50,7 +50,7 @@ 8.4 2.3 1.2 - 14.2.0 + 14.2.1-FOC1 7.4 9.0 14.5.2-FOC1 From 2104d341b0161b32c4bac9b95707378f53577a9a Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 9 Oct 2024 17:17:01 +0200 Subject: [PATCH 28/36] Refactor IsTableManaged --- .../server/deploy/BeanDescriptor.java | 7 ------- .../server/deploy/BeanDescriptorManager.java | 7 +++++-- .../server/deploy/BeanPropertyAssocMany.java | 16 ++++++++++++--- .../deploy/meta/DeployBeanDescriptor.java | 7 +++++++ .../meta/DeployBeanPropertyAssocMany.java | 4 ++++ .../server/persist/SaveManyBeans.java | 20 +++++++++++++++++++ 6 files changed, 49 insertions(+), 12 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java index e4a8d36b34..3217e7e723 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptor.java @@ -1899,13 +1899,6 @@ public BeanDescriptor descriptor(Class otherType) { return owner.descriptor(otherType); } - /** - * Returns true, if the table is managed (i.e. an existing m2m relation). - */ - public boolean isTableManaged(String tableName) { - return owner.isTableManaged(tableName); - } - /** * Return the order column property. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java index b9e16e3f69..1d856b69c7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanDescriptorManager.java @@ -90,6 +90,7 @@ public final class BeanDescriptorManager implements BeanDescriptorMap, SpiBeanTy private final BootupClasses bootupClasses; private final String serverName; private final List> elementDescriptors = new ArrayList<>(); + private final Set managedTables = new HashSet<>(); private final Map, BeanTable> beanTableMap = new HashMap<>(); private final Map> descMap = new HashMap<>(); private final Map> descQueueMap = new HashMap<>(); @@ -428,8 +429,7 @@ public List> beanTypes(String tableName) { @Override public boolean isTableManaged(String tableName) { - return tableToDescMap.get(tableName.toLowerCase()) != null - || tableToViewDescMap.get(tableName.toLowerCase()) != null; + return managedTables.contains(tableName); } /** @@ -699,6 +699,9 @@ private void readEntityBeanTable() { for (DeployBeanInfo info : deployInfoMap.values()) { BeanTable beanTable = createBeanTable(info); beanTableMap.put(beanTable.getBeanType(), beanTable); + if (beanTable.getBaseTable() != null) { + managedTables.add(beanTable.getBaseTable()); + } } // register non-id embedded beans (after bean tables are created) for (DeployBeanInfo info : embeddedBeans) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java index 6666ee2182..7860293ff2 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/BeanPropertyAssocMany.java @@ -38,6 +38,7 @@ public class BeanPropertyAssocMany extends BeanPropertyAssoc implements ST * Join for manyToMany intersection table. */ private final TableJoin intersectionJoin; + private final boolean tableManaged; private final String intersectionPublishTable; private final String intersectionDraftTable; private final boolean orphanRemoval; @@ -100,9 +101,11 @@ public BeanPropertyAssocMany(BeanDescriptor descriptor, DeployBeanPropertyAss this.fetchOrderBy = deploy.getFetchOrderBy(); this.intersectionJoin = deploy.createIntersectionTableJoin(); if (intersectionJoin != null) { + this.tableManaged = deploy.isTableManaged(); this.intersectionPublishTable = intersectionJoin.getTable(); this.intersectionDraftTable = deploy.getIntersectionDraftTable(); } else { + this.tableManaged = false; this.intersectionPublishTable = null; this.intersectionDraftTable = null; } @@ -986,11 +989,11 @@ public boolean isIncludeCascadeSave() { // Note ManyToMany always included as we always 'save' // the relationship via insert/delete of intersection table // REMOVALS means including PrivateOwned relationships - return cascadeInfo.isSave() || hasJoinTable() || ModifyListenMode.REMOVALS == modifyListenMode; + return cascadeInfo.isSave() || (hasJoinTable() && !tableManaged) || ModifyListenMode.REMOVALS == modifyListenMode; } public boolean isIncludeCascadeDelete() { - return cascadeInfo.isDelete() || hasJoinTable() || ModifyListenMode.REMOVALS == modifyListenMode; + return cascadeInfo.isDelete() || (hasJoinTable() && !tableManaged) || ModifyListenMode.REMOVALS == modifyListenMode; } boolean isCascadeDeleteEscalate() { @@ -1118,13 +1121,20 @@ public void bindElementValue(SqlUpdate insert, Object value) { targetDescriptor.bindElementValue(insert, value); } + /** + * Returns true, if this M2M beanproperty has a jointable, where the jointable is managed by an other entity. + */ + public boolean isTableManaged() { + return tableManaged; + } + /** * Returns true, if we must create a m2m join table. */ public boolean createJoinTable() { if (hasJoinTable() && mappedBy() == null) { // only create on other 'owning' side - return !descriptor.isTableManaged(intersectionJoin.getTable()); + return !tableManaged; } else { return false; } diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index 69f3dd628d..72659d5d3e 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -39,6 +39,13 @@ public class DeployBeanDescriptor implements DeployBeanDescriptorMeta { private static final Map EMPTY_RAW_MAP = new HashMap<>(); + /** + * Returns true, if the table is managed (i.e. an existing m2m relation). + */ + public boolean isTableManaged(String tableName) { + return manager.isTableManaged(tableName); + } + private static class PropOrder implements Comparator { @Override diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java index c201308e2d..edd41640c3 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/meta/DeployBeanPropertyAssocMany.java @@ -113,6 +113,10 @@ public TableJoin createIntersectionTableJoin() { } } + public boolean isTableManaged() { + return intersectionJoin != null && desc.isTableManaged(intersectionJoin.getTable()); + } + /** * Create the immutable version of the inverse join. */ diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java index b6e5d8a960..3a38865c24 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java @@ -9,8 +9,12 @@ import io.ebeaninternal.server.deploy.*; import jakarta.persistence.PersistenceException; + +import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -94,6 +98,22 @@ private boolean isSaveIntersection() { // OneToMany JoinTable return true; } + if (many.isTableManaged()) { + List tables = new ArrayList<>(3); + tables.add(many.descriptor().baseTable()); + tables.add(many.targetDescriptor().baseTable()); + tables.add(many.intersectionTableJoin().getTable()); + // put all tables in a deterministic order + tables.sort(Comparator.naturalOrder()); + + if (transaction.isSaveAssocManyIntersection(String.join("-", tables), many.descriptor().rootName())) { + // notify others, that we do save this transaction + transaction.isSaveAssocManyIntersection(many.intersectionTableJoin().getTable(), many.descriptor().rootName()); + return true; + } else { + return false; + } + } return transaction.isSaveAssocManyIntersection(many.intersectionTableJoin().getTable(), many.descriptor().rootName()); } From fdd6e79a64488389542bca41d45109136b447601 Mon Sep 17 00:00:00 2001 From: Roland Praml Date: Wed, 9 Oct 2024 17:17:28 +0200 Subject: [PATCH 29/36] Support for "extraWhere" --- .../server/deploy/IntersectionRow.java | 21 +++++- .../server/persist/DefaultPersister.java | 4 +- .../server/persist/SaveManyBeans.java | 2 +- .../java/org/tests/model/m2m/MnyEdge.java | 16 ++++ .../java/org/tests/model/m2m/MnyNode.java | 5 +- .../org/tests/model/m2m/TestM2MWithWhere.java | 74 +++++++++++++++++++ 6 files changed, 115 insertions(+), 7 deletions(-) diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java index 1475ef636d..131bfafea7 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/deploy/IntersectionRow.java @@ -66,7 +66,7 @@ public SpiSqlUpdate createInsert(SpiEbeanServer server) { return new DefaultSqlUpdate(server, sb.toString(), bindParams); } - public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode) { + public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode, String extraWhere) { BindParams bindParams = new BindParams(); StringBuilder sb = new StringBuilder(); if (deleteMode.isHard()) { @@ -90,17 +90,34 @@ public SpiSqlUpdate createDelete(SpiEbeanServer server, DeleteMode deleteMode) { bindParams.setParameter(++count, bindValue); } } + addExtraWhere(sb, extraWhere); + return new DefaultSqlUpdate(server, sb.toString(), bindParams); } - public SpiSqlUpdate createDeleteChildren(SpiEbeanServer server) { + + public SpiSqlUpdate createDeleteChildren(SpiEbeanServer server, String extraWhere) { BindParams bindParams = new BindParams(); StringBuilder sb = new StringBuilder(); sb.append("delete from ").append(tableName).append(" where "); setBindParams(bindParams, sb); + addExtraWhere(sb, extraWhere); return new DefaultSqlUpdate(server, sb.toString(), bindParams); } + private void addExtraWhere(StringBuilder sb, String extraWhere) { + if (extraWhere != null) { + if (extraWhere.indexOf("${ta}") == -1) { + // no table alias append ${mta} to query. + sb.append(" and ").append(extraWhere.replace("${mta}", tableName)); + } else if (extraWhere.indexOf("${mta}") != -1) { + // we have a table alias - this is not interesting for deletion. + // but if have also a m2m table alias - this is a problem now! + throw new UnsupportedOperationException("extraWhere \'" + extraWhere + "\' has both ${ta} and ${mta} - this is not yet supported"); + } + } + } + private int setBindParams(BindParams bindParams, StringBuilder sb) { int count = 0; for (Map.Entry entry : values.entrySet()) { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java index 99faaaf355..e0513c560a 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/DefaultPersister.java @@ -1002,7 +1002,7 @@ void deleteManyIntersection(EntityBean bean, BeanPropertyAssocMany many, SpiT private SpiSqlUpdate deleteAllIntersection(EntityBean bean, BeanPropertyAssocMany many, boolean publish) { IntersectionRow intRow = many.buildManyToManyDeleteChildren(bean, publish); - return intRow.createDeleteChildren(server); + return intRow.createDeleteChildren(server, many.extraWhere()); } /** @@ -1100,7 +1100,7 @@ void deleteManyDetails(SpiTransaction t, BeanDescriptor desc, EntityBean pare && (excludeDetailIds == null || excludeDetailIds.size() <= batch)) { // Just delete all the children with one statement IntersectionRow intRow = many.buildManyDeleteChildren(parentBean, excludeDetailIds); - SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode); + SqlUpdate sqlDelete = intRow.createDelete(server, deleteMode, many.extraWhere()); executeSqlUpdate(sqlDelete, t); } else { diff --git a/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java b/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java index 3a38865c24..2ff312e5f0 100644 --- a/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java +++ b/ebean-core/src/main/java/io/ebeaninternal/server/persist/SaveManyBeans.java @@ -319,7 +319,7 @@ private void saveAssocManyIntersection(boolean queue) { // the object from the 'other' side of the ManyToMany // build a intersection row for 'delete' IntersectionRow intRow = many.buildManyToManyMapBean(parentBean, otherDelete, publish); - SpiSqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD); + SpiSqlUpdate sqlDelete = intRow.createDelete(server, DeleteMode.HARD, many.extraWhere()); persister.executeOrQueue(sqlDelete, transaction, queue, BatchControl.DELETE_QUEUE); } } diff --git a/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java b/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java index 5d1b608617..50656f385c 100644 --- a/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java +++ b/ebean-test/src/test/java/org/tests/model/m2m/MnyEdge.java @@ -20,6 +20,22 @@ public class MnyEdge { @ManyToOne private MnyNode to; + + public MnyEdge() { +// default + } + + public MnyEdge(Object from, Object to) { + this.from = (MnyNode) from; + this.to = (MnyNode) to; + this.id = this.from.id * 10000 + this.to.id; + this.flags = this.from.id + this.to.id; + } + + public static MnyEdge createReverseRelation(Object to, MnyNode from) { + return new MnyEdge(from, to); + } + private int flags; public Integer getId() { diff --git a/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java b/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java index 6fbe29ba17..770ffa3db4 100644 --- a/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java +++ b/ebean-test/src/test/java/org/tests/model/m2m/MnyNode.java @@ -16,13 +16,14 @@ public class MnyNode { String name; - @ManyToMany + @ManyToMany(cascade = CascadeType.ALL) @JoinTable(name = "mny_edge", joinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id")) + @Where(clause = "${mta}.flags != 12345 and '${dbTableName}' = 'mny_node'") List allRelations; - @ManyToMany + @ManyToMany(cascade = CascadeType.ALL) @JoinTable(name = "mny_edge", joinColumns = @JoinColumn(name = "to_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "from_id", referencedColumnName = "id")) diff --git a/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java b/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java index d9ac37ffcc..d33c8f8e17 100644 --- a/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java +++ b/ebean-test/src/test/java/org/tests/model/m2m/TestM2MWithWhere.java @@ -4,6 +4,7 @@ import io.ebean.DB; import io.ebean.test.LoggedSql; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.List; @@ -18,6 +19,79 @@ */ public class TestM2MWithWhere extends BaseTestCase { + @Test + @Disabled + public void testModify() throws Exception { + + MnyNode node1 = new MnyNode(); + node1.setName("node1"); + node1.setId(111); + MnyNode node2 = new MnyNode(); + node2.setName("node2"); + node2.setId(222); + MnyNode node3 = new MnyNode(); + node3.setName("node3"); + node3.setId(333); + MnyNode node4 = new MnyNode(); + node4.setName("node4"); + node4.setId(444); + + node1.getAllReverseRelations().add(node2); + node1.getAllRelations().add(node2); + node2.getAllRelations().add(node3); + node3.getAllRelations().add(node4); + DB.save(node1); + DB.save(node1); + + DB.refresh(node2); + DB.refresh(node3); + assertThat(node2.getAllRelations()).containsExactlyInAnyOrder(node1, node3); + assertThat(node3.getAllReverseRelations()).containsExactlyInAnyOrder(node2); + + DB.refresh(node1); + node1.getAllReverseRelations().clear(); + System.out.println("Clearing"); + DB.save(node1); + DB.refresh(node2); + assertThat(node2.getAllRelations()).containsExactlyInAnyOrder(node3); + + node2.getAllRelations().clear(); + node2.getAllRelations().add(node3); + LoggedSql.start(); + DB.save(node2); + LoggedSql.stop().forEach(System.out::println); + + } + + @Test + @Disabled + public void testAccessAndModify() throws Exception { + createTestData(); + + MnyNode node = DB.find(MnyNode.class, 1); + node.setName("fooBarBaz"); + MnyNode removed = node.getAllRelations().remove(0); + + LoggedSql.start(); + DB.save(node); + List sql = LoggedSql.stop(); + assertThat(sql).hasSize(4); + assertThat(sql.get(0)).contains("update mny_node set name=? where id=?; -- bind(fooBarBaz"); + assertThat(sql.get(1)).contains("delete from mny_edge where from_id = ? and to_id = ? and mny_edge.flags != 12345 and 'mny_node' = 'mny_node'"); + assertThat(sql.get(2)).contains("-- bind"); + assertThat(sql.get(3)).contains("executeBatch() size:1 sql:delete from mny_edge where from_id = ? and to_id = ? and mny_edge.flags != 12345 and 'mny_node' = 'mny_node'"); + + node.getAllRelations().add(removed); + LoggedSql.start(); + DB.save(node); + sql = LoggedSql.stop(); + assertThat(sql).hasSize(3); + assertThat(sql.get(0)).contains("insert into mny_edge (id, flags, from_id, to_id) values (?,?,?,?)"); + assertThat(sql.get(1)).contains("-- bind"); + assertThat(sql.get(2)).contains("-- executeBatch() size:1 sql:insert into mny_edge (id, flags, from_id, to_id) values (?,?,?,?)"); + + } + @Test public void testQuery() throws Exception { createTestData(); From d45e5cbde25cafe76d327f56dc764fe45e6a8ba9 Mon Sep 17 00:00:00 2001 From: Noemi Praml Date: Fri, 18 Oct 2024 08:34:38 +0200 Subject: [PATCH 30/36] update to FOC-version --- composites/ebean-clickhouse/pom.xml | 10 +++--- composites/ebean-cockroach/pom.xml | 10 +++--- composites/ebean-db2/pom.xml | 10 +++--- composites/ebean-h2/pom.xml | 10 +++--- composites/ebean-hana/pom.xml | 10 +++--- composites/ebean-mariadb/pom.xml | 10 +++--- composites/ebean-mysql/pom.xml | 10 +++--- composites/ebean-net-postgis/pom.xml | 12 +++---- composites/ebean-nuodb/pom.xml | 10 +++--- composites/ebean-oracle/pom.xml | 10 +++--- composites/ebean-postgis/pom.xml | 12 +++---- composites/ebean-postgres/pom.xml | 10 +++--- composites/ebean-sqlite/pom.xml | 10 +++--- composites/ebean-sqlserver/pom.xml | 10 +++--- composites/ebean-yugabyte/pom.xml | 10 +++--- composites/ebean/pom.xml | 12 +++---- composites/pom.xml | 2 +- ebean-api/pom.xml | 2 +- ebean-bom/pom.xml | 52 ++++++++++++++-------------- ebean-core-type/pom.xml | 4 +-- ebean-core/pom.xml | 12 +++---- ebean-ddl-generator/pom.xml | 8 ++--- ebean-jackson-mapper/pom.xml | 4 +-- ebean-net-postgis-types/pom.xml | 8 ++--- ebean-postgis-types/pom.xml | 8 ++--- ebean-querybean/pom.xml | 10 +++--- ebean-redis/pom.xml | 12 +++---- ebean-spring-txn/pom.xml | 6 ++-- ebean-test/pom.xml | 12 +++---- kotlin-querybean-generator/pom.xml | 10 +++--- platforms/all/pom.xml | 28 +++++++-------- platforms/clickhouse/pom.xml | 4 +-- platforms/db2/pom.xml | 4 +-- platforms/h2/pom.xml | 4 +-- platforms/hana/pom.xml | 4 +-- platforms/hsqldb/pom.xml | 4 +-- platforms/mariadb/pom.xml | 6 ++-- platforms/mysql/pom.xml | 4 +-- platforms/nuodb/pom.xml | 4 +-- platforms/oracle/pom.xml | 4 +-- platforms/pom.xml | 2 +- platforms/postgres/pom.xml | 4 +-- platforms/sqlanywhere/pom.xml | 4 +-- platforms/sqlite/pom.xml | 4 +-- platforms/sqlserver/pom.xml | 4 +-- pom.xml | 2 +- querybean-generator/pom.xml | 2 +- tests/pom.xml | 2 +- tests/test-java16/pom.xml | 8 ++--- tests/test-kotlin/pom.xml | 6 ++-- 50 files changed, 210 insertions(+), 210 deletions(-) diff --git a/composites/ebean-clickhouse/pom.xml b/composites/ebean-clickhouse/pom.xml index 9fc8721bb9..a1c0aac0fb 100644 --- a/composites/ebean-clickhouse/pom.xml +++ b/composites/ebean-clickhouse/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-clickhouse - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-cockroach/pom.xml b/composites/ebean-cockroach/pom.xml index 80b1386179..0149f62095 100644 --- a/composites/ebean-cockroach/pom.xml +++ b/composites/ebean-cockroach/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-postgres - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-db2/pom.xml b/composites/ebean-db2/pom.xml index 5df4c53828..889e10ad1f 100644 --- a/composites/ebean-db2/pom.xml +++ b/composites/ebean-db2/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-db2 - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-h2/pom.xml b/composites/ebean-h2/pom.xml index 4178bb79f0..2726e2e2dc 100644 --- a/composites/ebean-h2/pom.xml +++ b/composites/ebean-h2/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-h2 - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-hana/pom.xml b/composites/ebean-hana/pom.xml index 5337f70961..7a3dace7c0 100644 --- a/composites/ebean-hana/pom.xml +++ b/composites/ebean-hana/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-hana - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-mariadb/pom.xml b/composites/ebean-mariadb/pom.xml index b5a8958272..589977a427 100644 --- a/composites/ebean-mariadb/pom.xml +++ b/composites/ebean-mariadb/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-mariadb - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-mysql/pom.xml b/composites/ebean-mysql/pom.xml index f6569f84fd..3838a0ee14 100644 --- a/composites/ebean-mysql/pom.xml +++ b/composites/ebean-mysql/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-mysql - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-net-postgis/pom.xml b/composites/ebean-net-postgis/pom.xml index 645f721066..bd88e5cd6a 100644 --- a/composites/ebean-net-postgis/pom.xml +++ b/composites/ebean-net-postgis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -22,13 +22,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -47,19 +47,19 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-postgres - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-net-postgis-types - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-nuodb/pom.xml b/composites/ebean-nuodb/pom.xml index 6a8e4a6b50..1100a66267 100644 --- a/composites/ebean-nuodb/pom.xml +++ b/composites/ebean-nuodb/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-nuodb - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-oracle/pom.xml b/composites/ebean-oracle/pom.xml index d48077d45f..9c8887bdee 100644 --- a/composites/ebean-oracle/pom.xml +++ b/composites/ebean-oracle/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-oracle - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-postgis/pom.xml b/composites/ebean-postgis/pom.xml index 018ae44b64..74a4af6e5b 100644 --- a/composites/ebean-postgis/pom.xml +++ b/composites/ebean-postgis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -22,13 +22,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -47,19 +47,19 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-postgres - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-postgis-types - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-postgres/pom.xml b/composites/ebean-postgres/pom.xml index 90e642d1fa..641ecfc29d 100644 --- a/composites/ebean-postgres/pom.xml +++ b/composites/ebean-postgres/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-postgres - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-sqlite/pom.xml b/composites/ebean-sqlite/pom.xml index c78c2f54fd..c43926e720 100644 --- a/composites/ebean-sqlite/pom.xml +++ b/composites/ebean-sqlite/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-sqlite - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-sqlserver/pom.xml b/composites/ebean-sqlserver/pom.xml index 31b75a8b04..eda69877a1 100644 --- a/composites/ebean-sqlserver/pom.xml +++ b/composites/ebean-sqlserver/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-sqlserver - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean-yugabyte/pom.xml b/composites/ebean-yugabyte/pom.xml index ca4a1a1db8..ba9a22020e 100644 --- a/composites/ebean-yugabyte/pom.xml +++ b/composites/ebean-yugabyte/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -42,13 +42,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-postgres - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/ebean/pom.xml b/composites/ebean/pom.xml index 966fd0dc4b..0e2d1bdc25 100644 --- a/composites/ebean/pom.xml +++ b/composites/ebean/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -17,13 +17,13 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -41,7 +41,7 @@ io.ebean ebean-jackson-mapper - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -60,13 +60,13 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-all - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/composites/pom.xml b/composites/pom.xml index bd187d6feb..a324f58584 100644 --- a/composites/pom.xml +++ b/composites/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT composites diff --git a/ebean-api/pom.xml b/ebean-api/pom.xml index bd763a5fbf..4e88927e49 100644 --- a/ebean-api/pom.xml +++ b/ebean-api/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean api diff --git a/ebean-bom/pom.xml b/ebean-bom/pom.xml index a28002ad41..c5ca4f0352 100644 --- a/ebean-bom/pom.xml +++ b/ebean-bom/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean bom @@ -89,25 +89,25 @@ io.ebean ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core-type - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -125,13 +125,13 @@ io.ebean ebean-jackson-mapper - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-ddl-generator - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -155,37 +155,37 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean querybean-generator - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean kotlin-querybean-generator - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-test - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-redis - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-spring-txn - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -193,79 +193,79 @@ io.ebean ebean-clickhouse - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-db2 - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-h2 - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-hana - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-mariadb - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-mysql - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-nuodb - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-oracle - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-postgres - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-postgis - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-postgis-types - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-sqlite - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-sqlserver - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/ebean-core-type/pom.xml b/ebean-core-type/pom.xml index 6fa566dee7..0c84c0a7c9 100644 --- a/ebean-core-type/pom.xml +++ b/ebean-core-type/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean-core-type @@ -16,7 +16,7 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/ebean-core/pom.xml b/ebean-core/pom.xml index 81b6fafa9f..8cd5d865bb 100644 --- a/ebean-core/pom.xml +++ b/ebean-core/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean-core @@ -22,7 +22,7 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -46,7 +46,7 @@ io.ebean ebean-core-type - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -159,21 +159,21 @@ io.ebean ebean-platform-h2 - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test io.ebean ebean-platform-postgres - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test io.ebean ebean-platform-sqlserver - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test diff --git a/ebean-ddl-generator/pom.xml b/ebean-ddl-generator/pom.xml index 175bac43c3..5dbd69ac6e 100644 --- a/ebean-ddl-generator/pom.xml +++ b/ebean-ddl-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean ddl generation @@ -28,14 +28,14 @@ io.ebean ebean-core-type - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided @@ -65,7 +65,7 @@ io.ebean ebean-platform-all - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test diff --git a/ebean-jackson-mapper/pom.xml b/ebean-jackson-mapper/pom.xml index 5b6f20e3a6..f324702044 100644 --- a/ebean-jackson-mapper/pom.xml +++ b/ebean-jackson-mapper/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT 4.0.0 @@ -14,7 +14,7 @@ io.ebean ebean-core-type - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided diff --git a/ebean-net-postgis-types/pom.xml b/ebean-net-postgis-types/pom.xml index 897102eb02..c06660b4ce 100644 --- a/ebean-net-postgis-types/pom.xml +++ b/ebean-net-postgis-types/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean net postgis types @@ -19,14 +19,14 @@ io.ebean ebean-platform-postgres - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided @@ -54,7 +54,7 @@ io.ebean ebean-test - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test diff --git a/ebean-postgis-types/pom.xml b/ebean-postgis-types/pom.xml index 675952606d..3819ae302c 100644 --- a/ebean-postgis-types/pom.xml +++ b/ebean-postgis-types/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean postgis types @@ -19,14 +19,14 @@ io.ebean ebean-platform-postgres - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided @@ -62,7 +62,7 @@ io.ebean ebean-test - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test diff --git a/ebean-querybean/pom.xml b/ebean-querybean/pom.xml index bd061d5976..9bcddc8cd1 100644 --- a/ebean-querybean/pom.xml +++ b/ebean-querybean/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean querybean @@ -17,7 +17,7 @@ io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided @@ -59,14 +59,14 @@ io.ebean ebean-ddl-generator - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test io.ebean ebean-test - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test @@ -80,7 +80,7 @@ io.ebean querybean-generator - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided diff --git a/ebean-redis/pom.xml b/ebean-redis/pom.xml index 2ebf287546..16cca1981a 100644 --- a/ebean-redis/pom.xml +++ b/ebean-redis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean-redis @@ -29,35 +29,35 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test io.ebean ebean-test - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test io.ebean querybean-generator - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided diff --git a/ebean-spring-txn/pom.xml b/ebean-spring-txn/pom.xml index 2b2bd13ca4..b6de4429c5 100644 --- a/ebean-spring-txn/pom.xml +++ b/ebean-spring-txn/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean-spring-txn @@ -28,7 +28,7 @@ io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided @@ -77,7 +77,7 @@ io.ebean ebean-test - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test diff --git a/ebean-test/pom.xml b/ebean-test/pom.xml index fc68ec53a6..8362239392 100644 --- a/ebean-test/pom.xml +++ b/ebean-test/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ebean test @@ -33,20 +33,20 @@ io.ebean ebean-platform-h2 - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT provided io.ebean ebean-ddl-generator - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -149,14 +149,14 @@ io.ebean ebean-jackson-mapper - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test io.ebean ebean-platform-all - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test diff --git a/kotlin-querybean-generator/pom.xml b/kotlin-querybean-generator/pom.xml index 053155a567..043ec07eb1 100644 --- a/kotlin-querybean-generator/pom.xml +++ b/kotlin-querybean-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT kotlin querybean generator @@ -21,7 +21,7 @@ io.ebean ebean-querybean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test @@ -35,7 +35,7 @@ io.ebean ebean-core - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test @@ -56,14 +56,14 @@ io.ebean ebean-platform-h2 - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test io.ebean ebean-ddl-generator - 14.7.0 + 14.7.0-FOC1-SNAPSHOT test diff --git a/platforms/all/pom.xml b/platforms/all/pom.xml index 25289a4db1..6dd47dc5b8 100644 --- a/platforms/all/pom.xml +++ b/platforms/all/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -15,67 +15,67 @@ io.ebean ebean-platform-h2 - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-clickhouse - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-db2 - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-hana - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-hsqldb - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-mysql - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-mariadb - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-nuodb - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-oracle - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-postgres - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-sqlanywhere - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-sqlite - 14.7.0 + 14.7.0-FOC1-SNAPSHOT io.ebean ebean-platform-sqlserver - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/platforms/clickhouse/pom.xml b/platforms/clickhouse/pom.xml index 6b5922d5cc..dbb38e0f4a 100644 --- a/platforms/clickhouse/pom.xml +++ b/platforms/clickhouse/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -15,7 +15,7 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/platforms/db2/pom.xml b/platforms/db2/pom.xml index a2232b1537..883e899a2c 100644 --- a/platforms/db2/pom.xml +++ b/platforms/db2/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -15,7 +15,7 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT diff --git a/platforms/h2/pom.xml b/platforms/h2/pom.xml index eae553ca5e..3dfa1b36b7 100644 --- a/platforms/h2/pom.xml +++ b/platforms/h2/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0 + 14.7.0-FOC1-SNAPSHOT ../.. @@ -15,7 +15,7 @@ io.ebean ebean-api - 14.7.0 + 14.7.0-FOC1-SNAPSHOT @@ -193,79 +193,79 @@ io.ebean ebean-clickhouse - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-db2 - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-h2 - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-hana - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-mariadb - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-mysql - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-nuodb - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-oracle - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-postgres - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-postgis - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-postgis-types - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-sqlite - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-sqlserver - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 diff --git a/ebean-core-type/pom.xml b/ebean-core-type/pom.xml index 0c84c0a7c9..982d73ced7 100644 --- a/ebean-core-type/pom.xml +++ b/ebean-core-type/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ebean-core-type @@ -16,7 +16,7 @@ io.ebean ebean-api - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 diff --git a/ebean-core/pom.xml b/ebean-core/pom.xml index 8cd5d865bb..a6ebbfb7ab 100644 --- a/ebean-core/pom.xml +++ b/ebean-core/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ebean-core @@ -15,14 +15,14 @@ scm:git:git@github.com:ebean-orm/ebean.git - HEAD + ebean-parent-14.7.0-FOC1 io.ebean ebean-api - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 @@ -46,7 +46,7 @@ io.ebean ebean-core-type - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 @@ -159,21 +159,21 @@ io.ebean ebean-platform-h2 - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test io.ebean ebean-platform-postgres - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test io.ebean ebean-platform-sqlserver - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test diff --git a/ebean-ddl-generator/pom.xml b/ebean-ddl-generator/pom.xml index 5dbd69ac6e..0a083cf513 100644 --- a/ebean-ddl-generator/pom.xml +++ b/ebean-ddl-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ebean ddl generation @@ -28,14 +28,14 @@ io.ebean ebean-core-type - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided io.ebean ebean-core - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided @@ -65,7 +65,7 @@ io.ebean ebean-platform-all - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test diff --git a/ebean-jackson-mapper/pom.xml b/ebean-jackson-mapper/pom.xml index f324702044..3c3eb5e267 100644 --- a/ebean-jackson-mapper/pom.xml +++ b/ebean-jackson-mapper/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 4.0.0 @@ -14,7 +14,7 @@ io.ebean ebean-core-type - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided diff --git a/ebean-net-postgis-types/pom.xml b/ebean-net-postgis-types/pom.xml index c06660b4ce..e5c4288bd8 100644 --- a/ebean-net-postgis-types/pom.xml +++ b/ebean-net-postgis-types/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ebean net postgis types @@ -19,14 +19,14 @@ io.ebean ebean-platform-postgres - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-core - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided @@ -54,7 +54,7 @@ io.ebean ebean-test - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test diff --git a/ebean-postgis-types/pom.xml b/ebean-postgis-types/pom.xml index 3819ae302c..9894c81fc2 100644 --- a/ebean-postgis-types/pom.xml +++ b/ebean-postgis-types/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ebean postgis types @@ -19,14 +19,14 @@ io.ebean ebean-platform-postgres - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-core - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided @@ -62,7 +62,7 @@ io.ebean ebean-test - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test diff --git a/ebean-querybean/pom.xml b/ebean-querybean/pom.xml index 9bcddc8cd1..1284bb715d 100644 --- a/ebean-querybean/pom.xml +++ b/ebean-querybean/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ebean querybean @@ -17,7 +17,7 @@ io.ebean ebean-core - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided @@ -59,14 +59,14 @@ io.ebean ebean-ddl-generator - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test io.ebean ebean-test - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test @@ -80,7 +80,7 @@ io.ebean querybean-generator - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided diff --git a/ebean-redis/pom.xml b/ebean-redis/pom.xml index 16cca1981a..cbe3711bfd 100644 --- a/ebean-redis/pom.xml +++ b/ebean-redis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ebean-redis @@ -29,35 +29,35 @@ io.ebean ebean-api - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided io.ebean ebean-core - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided io.ebean ebean-querybean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test io.ebean ebean-test - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test io.ebean querybean-generator - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided diff --git a/ebean-spring-txn/pom.xml b/ebean-spring-txn/pom.xml index b6de4429c5..ce7b0bd48a 100644 --- a/ebean-spring-txn/pom.xml +++ b/ebean-spring-txn/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ebean-spring-txn @@ -28,7 +28,7 @@ io.ebean ebean-core - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided @@ -77,7 +77,7 @@ io.ebean ebean-test - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test diff --git a/ebean-test/pom.xml b/ebean-test/pom.xml index 8362239392..00b3123d9b 100644 --- a/ebean-test/pom.xml +++ b/ebean-test/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ebean test @@ -33,20 +33,20 @@ io.ebean ebean-platform-h2 - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-core - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 provided io.ebean ebean-ddl-generator - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 @@ -149,14 +149,14 @@ io.ebean ebean-jackson-mapper - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test io.ebean ebean-platform-all - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test diff --git a/kotlin-querybean-generator/pom.xml b/kotlin-querybean-generator/pom.xml index 043ec07eb1..b0a84837fd 100644 --- a/kotlin-querybean-generator/pom.xml +++ b/kotlin-querybean-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 kotlin querybean generator @@ -21,7 +21,7 @@ io.ebean ebean-querybean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test @@ -35,7 +35,7 @@ io.ebean ebean-core - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test @@ -56,14 +56,14 @@ io.ebean ebean-platform-h2 - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test io.ebean ebean-ddl-generator - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 test diff --git a/platforms/all/pom.xml b/platforms/all/pom.xml index 6dd47dc5b8..b64e6c55aa 100644 --- a/platforms/all/pom.xml +++ b/platforms/all/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ../.. @@ -15,67 +15,67 @@ io.ebean ebean-platform-h2 - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-clickhouse - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-db2 - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-hana - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-hsqldb - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-mysql - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-mariadb - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-nuodb - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-oracle - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-postgres - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-sqlanywhere - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-sqlite - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 io.ebean ebean-platform-sqlserver - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 diff --git a/platforms/clickhouse/pom.xml b/platforms/clickhouse/pom.xml index dbb38e0f4a..7020375fdf 100644 --- a/platforms/clickhouse/pom.xml +++ b/platforms/clickhouse/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ../.. @@ -15,7 +15,7 @@ io.ebean ebean-api - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 diff --git a/platforms/db2/pom.xml b/platforms/db2/pom.xml index 883e899a2c..fea831b044 100644 --- a/platforms/db2/pom.xml +++ b/platforms/db2/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ../.. @@ -15,7 +15,7 @@ io.ebean ebean-api - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 diff --git a/platforms/h2/pom.xml b/platforms/h2/pom.xml index 3dfa1b36b7..ff41c53015 100644 --- a/platforms/h2/pom.xml +++ b/platforms/h2/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 ../.. @@ -15,7 +15,7 @@ io.ebean ebean-api - 14.7.0-FOC1-SNAPSHOT + 14.7.0-FOC1 @@ -193,79 +193,79 @@ io.ebean ebean-clickhouse - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-db2 - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-h2 - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-hana - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-mariadb - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-mysql - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-nuodb - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-oracle - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-postgres - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-postgis - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-postgis-types - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-sqlite - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-sqlserver - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT diff --git a/ebean-core-type/pom.xml b/ebean-core-type/pom.xml index 982d73ced7..9e8b72f82c 100644 --- a/ebean-core-type/pom.xml +++ b/ebean-core-type/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ebean-core-type @@ -16,7 +16,7 @@ io.ebean ebean-api - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT diff --git a/ebean-core/pom.xml b/ebean-core/pom.xml index a6ebbfb7ab..6b9289db3b 100644 --- a/ebean-core/pom.xml +++ b/ebean-core/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ebean-core @@ -15,14 +15,14 @@ scm:git:git@github.com:ebean-orm/ebean.git - ebean-parent-14.7.0-FOC1 + HEAD io.ebean ebean-api - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT @@ -46,7 +46,7 @@ io.ebean ebean-core-type - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT @@ -159,21 +159,21 @@ io.ebean ebean-platform-h2 - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test io.ebean ebean-platform-postgres - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test io.ebean ebean-platform-sqlserver - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test diff --git a/ebean-ddl-generator/pom.xml b/ebean-ddl-generator/pom.xml index 0a083cf513..fcfff4652f 100644 --- a/ebean-ddl-generator/pom.xml +++ b/ebean-ddl-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ebean ddl generation @@ -28,14 +28,14 @@ io.ebean ebean-core-type - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided io.ebean ebean-core - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided @@ -65,7 +65,7 @@ io.ebean ebean-platform-all - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test diff --git a/ebean-jackson-mapper/pom.xml b/ebean-jackson-mapper/pom.xml index 3c3eb5e267..0abf2198a7 100644 --- a/ebean-jackson-mapper/pom.xml +++ b/ebean-jackson-mapper/pom.xml @@ -3,7 +3,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT 4.0.0 @@ -14,7 +14,7 @@ io.ebean ebean-core-type - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided diff --git a/ebean-net-postgis-types/pom.xml b/ebean-net-postgis-types/pom.xml index e5c4288bd8..84ca1acd12 100644 --- a/ebean-net-postgis-types/pom.xml +++ b/ebean-net-postgis-types/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ebean net postgis types @@ -19,14 +19,14 @@ io.ebean ebean-platform-postgres - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-core - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided @@ -54,7 +54,7 @@ io.ebean ebean-test - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test diff --git a/ebean-postgis-types/pom.xml b/ebean-postgis-types/pom.xml index 9894c81fc2..ded5dfe080 100644 --- a/ebean-postgis-types/pom.xml +++ b/ebean-postgis-types/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ebean postgis types @@ -19,14 +19,14 @@ io.ebean ebean-platform-postgres - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-core - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided @@ -62,7 +62,7 @@ io.ebean ebean-test - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test diff --git a/ebean-querybean/pom.xml b/ebean-querybean/pom.xml index 1284bb715d..192c25aee3 100644 --- a/ebean-querybean/pom.xml +++ b/ebean-querybean/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ebean querybean @@ -17,7 +17,7 @@ io.ebean ebean-core - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided @@ -59,14 +59,14 @@ io.ebean ebean-ddl-generator - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test io.ebean ebean-test - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test @@ -80,7 +80,7 @@ io.ebean querybean-generator - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided diff --git a/ebean-redis/pom.xml b/ebean-redis/pom.xml index cbe3711bfd..5b3323852a 100644 --- a/ebean-redis/pom.xml +++ b/ebean-redis/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ebean-redis @@ -29,35 +29,35 @@ io.ebean ebean-api - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided io.ebean ebean-core - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided io.ebean ebean-querybean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test io.ebean ebean-test - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test io.ebean querybean-generator - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided diff --git a/ebean-spring-txn/pom.xml b/ebean-spring-txn/pom.xml index ce7b0bd48a..5446fc616d 100644 --- a/ebean-spring-txn/pom.xml +++ b/ebean-spring-txn/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ebean-spring-txn @@ -28,7 +28,7 @@ io.ebean ebean-core - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided @@ -77,7 +77,7 @@ io.ebean ebean-test - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test diff --git a/ebean-test/pom.xml b/ebean-test/pom.xml index 00b3123d9b..321a7b73ba 100644 --- a/ebean-test/pom.xml +++ b/ebean-test/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ebean test @@ -33,20 +33,20 @@ io.ebean ebean-platform-h2 - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-core - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT provided io.ebean ebean-ddl-generator - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT @@ -149,14 +149,14 @@ io.ebean ebean-jackson-mapper - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test io.ebean ebean-platform-all - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test diff --git a/kotlin-querybean-generator/pom.xml b/kotlin-querybean-generator/pom.xml index b0a84837fd..b11de8a724 100644 --- a/kotlin-querybean-generator/pom.xml +++ b/kotlin-querybean-generator/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT kotlin querybean generator @@ -21,7 +21,7 @@ io.ebean ebean-querybean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test @@ -35,7 +35,7 @@ io.ebean ebean-core - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test @@ -56,14 +56,14 @@ io.ebean ebean-platform-h2 - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test io.ebean ebean-ddl-generator - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT test diff --git a/platforms/all/pom.xml b/platforms/all/pom.xml index b64e6c55aa..e9834aaa38 100644 --- a/platforms/all/pom.xml +++ b/platforms/all/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ../.. @@ -15,67 +15,67 @@ io.ebean ebean-platform-h2 - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-clickhouse - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-db2 - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-hana - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-hsqldb - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-mysql - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-mariadb - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-nuodb - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-oracle - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-postgres - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-sqlanywhere - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-sqlite - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT io.ebean ebean-platform-sqlserver - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT diff --git a/platforms/clickhouse/pom.xml b/platforms/clickhouse/pom.xml index 7020375fdf..7fd5e12568 100644 --- a/platforms/clickhouse/pom.xml +++ b/platforms/clickhouse/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ../.. @@ -15,7 +15,7 @@ io.ebean ebean-api - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT diff --git a/platforms/db2/pom.xml b/platforms/db2/pom.xml index fea831b044..2137b139dd 100644 --- a/platforms/db2/pom.xml +++ b/platforms/db2/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ../.. @@ -15,7 +15,7 @@ io.ebean ebean-api - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT diff --git a/platforms/h2/pom.xml b/platforms/h2/pom.xml index ff41c53015..b1c3c978ee 100644 --- a/platforms/h2/pom.xml +++ b/platforms/h2/pom.xml @@ -4,7 +4,7 @@ ebean-parent io.ebean - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT ../.. @@ -15,7 +15,7 @@ io.ebean ebean-api - 14.7.0-FOC1 + 14.7.0-FOC2-SNAPSHOT