Lower memory usage in FragmentRepository 91/134791/6
authordanielhanrahan <daniel.hanrahan@est.tech>
Thu, 8 Jun 2023 13:37:17 +0000 (14:37 +0100)
committerdanielhanrahan <daniel.hanrahan@est.tech>
Wed, 14 Jun 2023 13:29:06 +0000 (14:29 +0100)
Avoid using Spring Data "interface projection" in FragmentRepository.
The use of FragmentExtract in FragmentRepository is causing an
overhead of around 5 kilobytes per fragment, which is leading to
abnormally high memory usage when queries return a large number of
nodes. For example, around 250MB of additional memory is needlessly
used when fetching 50,000 datanodes.

- Remove FragmentExtract interface and FragmentEntityArranger class.
- Add FragmentPrefetchRepository, using JdbcTemplate and RowMapper
  to fetch FragmentEntity descendants in a single SQL query.
- Many CpsDataService operations have memory reductions:
  - queryDataNodes
  - getDataNodesForMultipleXpaths
  - updateDataNodesAndDescendants
  - updateNodeLeaves
  - and any NCMP methods using the above.

Issue-ID: CPS-1716
Signed-off-by: danielhanrahan <daniel.hanrahan@est.tech>
Change-Id: Ic47a2c9eb34150ed76bd5ce452fe1c9aaf9b4c5c

cps-ri/pom.xml
cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntityArranger.java [deleted file]
cps-ri/src/main/java/org/onap/cps/spi/impl/CpsDataPersistenceServiceImpl.java
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentPrefetchRepository.java [moved from cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentExtract.java with 65% similarity]
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentPrefetchRepositoryImpl.java [new file with mode: 0644]
cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentRepository.java
cps-ri/src/test/groovy/org/onap/cps/spi/impl/CpsDataPersistenceServiceSpec.groovy

index 66b89de..504fce5 100644 (file)
@@ -33,7 +33,7 @@
     <artifactId>cps-ri</artifactId>\r
 \r
     <properties>\r
-        <minimum-coverage>0.34</minimum-coverage>\r
+        <minimum-coverage>0.32</minimum-coverage>\r
         <!-- Additional coverage is provided by the integration-test module -->\r
     </properties>\r
 \r
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntityArranger.java b/cps-ri/src/main/java/org/onap/cps/spi/entities/FragmentEntityArranger.java
deleted file mode 100644 (file)
index 697eb8d..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- *  ============LICENSE_START=======================================================
- *  Copyright (C) 2022-2023 Nordix Foundation
- *  Modifications Copyright (C) 2023 TechMahindra Ltd.
- *  ================================================================================
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *        http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- *
- *  SPDX-License-Identifier: Apache-2.0
- *  ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.spi.entities;
-
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import lombok.AccessLevel;
-import lombok.NoArgsConstructor;
-
-@NoArgsConstructor(access = AccessLevel.PRIVATE)
-public class FragmentEntityArranger {
-
-    /**
-     * Convert a collection of (related) FragmentExtracts into  FragmentEntities (trees) with descendants.
-     *
-     * @param anchorEntity the anchor(entity) all the fragments belong to
-     * @param fragmentExtracts FragmentExtracts to convert
-     * @return a collection of FragmentEntities (trees) with descendants.
-     */
-    public static Collection<FragmentEntity> toFragmentEntityTrees(final AnchorEntity anchorEntity,
-                                                      final Collection<FragmentExtract> fragmentExtracts) {
-        final Map<Long, FragmentEntity> fragmentEntityPerId = new HashMap<>();
-        if (fragmentExtracts !=  null) {
-            for (final FragmentExtract fragmentExtract : fragmentExtracts) {
-                final FragmentEntity fragmentEntity = toFragmentEntity(anchorEntity, fragmentExtract);
-                fragmentEntityPerId.put(fragmentEntity.getId(), fragmentEntity);
-            }
-        }
-        return reuniteChildrenWithTheirParents(fragmentEntityPerId);
-    }
-
-    /**
-     * Convert a collection of (related) FragmentExtracts into  FragmentEntities (trees) with descendants.
-     *
-     * @param anchorEntityPerId the anchor(entities) the fragments belong to
-     * @param fragmentExtracts FragmentExtracts to convert
-     * @return a collection of FragmentEntities (trees) with descendants.
-     */
-    public static Collection<FragmentEntity> toFragmentEntityTreesAcrossAnchors(
-            final Map<Long, AnchorEntity> anchorEntityPerId, final Collection<FragmentExtract> fragmentExtracts) {
-        final Map<Long, FragmentEntity> fragmentEntityPerId = new HashMap<>();
-        for (final FragmentExtract fragmentExtract : fragmentExtracts) {
-            final AnchorEntity anchorEntity = anchorEntityPerId.get(fragmentExtract.getAnchorId());
-            final FragmentEntity fragmentEntity = toFragmentEntity(anchorEntity, fragmentExtract);
-            fragmentEntityPerId.put(fragmentEntity.getId(), fragmentEntity);
-        }
-        return reuniteChildrenWithTheirParents(fragmentEntityPerId);
-    }
-
-    private static FragmentEntity toFragmentEntity(final AnchorEntity anchorEntity,
-                                                   final FragmentExtract fragmentExtract) {
-        final FragmentEntity fragmentEntity = new FragmentEntity();
-        fragmentEntity.setAnchor(anchorEntity);
-        fragmentEntity.setId(fragmentExtract.getId());
-        fragmentEntity.setXpath(fragmentExtract.getXpath());
-        fragmentEntity.setAttributes(fragmentExtract.getAttributes());
-        fragmentEntity.setParentId(fragmentExtract.getParentId());
-        fragmentEntity.setChildFragments(new HashSet<>());
-        return fragmentEntity;
-    }
-
-    private static Collection<FragmentEntity> reuniteChildrenWithTheirParents(
-        final Map<Long, FragmentEntity> fragmentEntityPerId) {
-        final Collection<FragmentEntity> fragmentEntitiesWithoutParentInResultSet = new HashSet<>();
-        for (final FragmentEntity fragmentEntity : fragmentEntityPerId.values()) {
-            final FragmentEntity parentFragmentEntity = fragmentEntityPerId.get(fragmentEntity.getParentId());
-            if (parentFragmentEntity == null) {
-                fragmentEntitiesWithoutParentInResultSet.add(fragmentEntity);
-            } else {
-                parentFragmentEntity.getChildFragments().add(fragmentEntity);
-            }
-        }
-        return fragmentEntitiesWithoutParentInResultSet;
-    }
-
-}
index 02f7230..e6e250f 100644 (file)
@@ -36,7 +36,6 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -52,8 +51,6 @@ import org.onap.cps.spi.FetchDescendantsOption;
 import org.onap.cps.spi.entities.AnchorEntity;
 import org.onap.cps.spi.entities.DataspaceEntity;
 import org.onap.cps.spi.entities.FragmentEntity;
-import org.onap.cps.spi.entities.FragmentEntityArranger;
-import org.onap.cps.spi.entities.FragmentExtract;
 import org.onap.cps.spi.exceptions.AlreadyDefinedException;
 import org.onap.cps.spi.exceptions.AlreadyDefinedExceptionBatch;
 import org.onap.cps.spi.exceptions.ConcurrencyException;
@@ -248,7 +245,7 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         final AnchorEntity anchorEntity = getAnchorEntity(dataspaceName, anchorName);
         final Collection<FragmentEntity> fragmentEntities =
             getFragmentEntities(anchorEntity, xpaths, fetchDescendantsOption);
-        return toDataNodes(fragmentEntities, fetchDescendantsOption);
+        return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
     }
 
     private Collection<FragmentEntity> getFragmentEntities(final AnchorEntity anchorEntity,
@@ -269,19 +266,16 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
             normalizedXpaths.addAll(fragmentRepository.findAllXpathByAnchorAndParentIdIsNull(anchorEntity));
         }
 
-        final List<FragmentExtract> fragmentExtracts =
-            fragmentRepository.findExtractsWithDescendants(anchorEntity.getId(), normalizedXpaths,
-                fetchDescendantsOption.getDepth());
+        final List<FragmentEntity> fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity,
+                normalizedXpaths);
 
-        return FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts);
+        return fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption, fragmentEntities);
     }
 
     private FragmentEntity getFragmentEntity(final AnchorEntity anchorEntity, final String xpath) {
         final FragmentEntity fragmentEntity;
         if (isRootXpath(xpath)) {
-            final List<FragmentExtract> fragmentExtracts = fragmentRepository.findAllExtractsByAnchor(anchorEntity);
-            fragmentEntity = FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts)
-                .stream().findFirst().orElse(null);
+            fragmentEntity = fragmentRepository.findOneByAnchorId(anchorEntity.getId()).orElse(null);
         } else {
             fragmentEntity = fragmentRepository.getByAnchorAndXpath(anchorEntity, getNormalizedXpath(xpath));
         }
@@ -320,8 +314,8 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
                 fragmentEntities = fragmentRepository.findByAnchorAndXpathIn(anchorEntity, ancestorXpaths);
             }
         }
-        fragmentEntities = prefetchDescendantsForFragmentEntities(fetchDescendantsOption, anchorEntity,
-            fragmentEntities);
+        fragmentEntities = fragmentRepository.prefetchDescendantsOfFragmentEntities(fetchDescendantsOption,
+                fragmentEntities);
         return createDataNodesFromFragmentEntities(fetchDescendantsOption, fragmentEntities);
     }
 
@@ -331,31 +325,6 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
         return queryDataNodes(dataspaceName, QUERY_ACROSS_ANCHORS, cpsPath, fetchDescendantsOption);
     }
 
-    private Collection<FragmentEntity> prefetchDescendantsForFragmentEntities(
-                                            final FetchDescendantsOption fetchDescendantsOption,
-                                            final AnchorEntity anchorEntity,
-                                            final Collection<FragmentEntity> proxiedFragmentEntities) {
-        if (FetchDescendantsOption.OMIT_DESCENDANTS.equals(fetchDescendantsOption)) {
-            return proxiedFragmentEntities;
-        }
-
-        final List<Long> fragmentEntityIds = proxiedFragmentEntities.stream()
-            .map(FragmentEntity::getId).collect(Collectors.toList());
-
-        final List<FragmentExtract> fragmentExtracts =
-            fragmentRepository.findExtractsWithDescendantsByIds(fragmentEntityIds, fetchDescendantsOption.getDepth());
-
-        if (anchorEntity == ALL_ANCHORS) {
-            final Collection<Long> anchorIds = fragmentExtracts.stream()
-                .map(FragmentExtract::getAnchorId).collect(Collectors.toSet());
-            final List<AnchorEntity> anchorEntities = anchorRepository.findAllById(anchorIds);
-            final Map<Long, AnchorEntity> anchorEntityPerId = anchorEntities.stream()
-                .collect(Collectors.toMap(AnchorEntity::getId, Function.identity()));
-            return FragmentEntityArranger.toFragmentEntityTreesAcrossAnchors(anchorEntityPerId, fragmentExtracts);
-        }
-        return FragmentEntityArranger.toFragmentEntityTrees(anchorEntity, fragmentExtracts);
-    }
-
     private List<DataNode> createDataNodesFromFragmentEntities(final FetchDescendantsOption fetchDescendantsOption,
                                                                final Collection<FragmentEntity> fragmentEntities) {
         final List<DataNode> dataNodes = new ArrayList<>(fragmentEntities.size());
@@ -422,15 +391,6 @@ public class CpsDataPersistenceServiceImpl implements CpsDataPersistenceService
                 .withChildDataNodes(childDataNodes).build();
     }
 
-    private Collection<DataNode> toDataNodes(final Collection<FragmentEntity> fragmentEntities,
-                                             final FetchDescendantsOption fetchDescendantsOption) {
-        final Collection<DataNode> dataNodes = new ArrayList<>(fragmentEntities.size());
-        for (final FragmentEntity fragmentEntity : fragmentEntities) {
-            dataNodes.add(toDataNode(fragmentEntity, fetchDescendantsOption));
-        }
-        return dataNodes;
-    }
-
     private List<DataNode> getChildDataNodes(final FragmentEntity fragmentEntity,
                                              final FetchDescendantsOption fetchDescendantsOption) {
         if (fetchDescendantsOption.hasNext()) {
@@ -1,6 +1,6 @@
 /*
  * ============LICENSE_START=======================================================
- * Copyright (C) 2022-2023 Nordix Foundation.
+ * Copyright (C) 2023 Nordix Foundation.
  * ================================================================================
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * ============LICENSE_END=========================================================
  */
 
-package org.onap.cps.spi.entities;
+package org.onap.cps.spi.repository;
 
-public interface FragmentExtract {
+import java.util.Collection;
+import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.entities.FragmentEntity;
 
-    Long getId();
-
-    Long getAnchorId();
-
-    String getXpath();
-
-    Long getParentId();
-
-    String getAttributes();
+public interface FragmentPrefetchRepository {
+    Collection<FragmentEntity> prefetchDescendantsOfFragmentEntities(
+            final FetchDescendantsOption fetchDescendantsOption,
+            final Collection<FragmentEntity> proxiedFragmentEntities);
 }
diff --git a/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentPrefetchRepositoryImpl.java b/cps-ri/src/main/java/org/onap/cps/spi/repository/FragmentPrefetchRepositoryImpl.java
new file mode 100644 (file)
index 0000000..4f056c8
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (C) 2023 Nordix Foundation.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.spi.repository;
+
+import java.sql.Connection;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.RequiredArgsConstructor;
+import org.onap.cps.spi.FetchDescendantsOption;
+import org.onap.cps.spi.entities.AnchorEntity;
+import org.onap.cps.spi.entities.FragmentEntity;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.PreparedStatementSetter;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class FragmentPrefetchRepositoryImpl implements FragmentPrefetchRepository {
+
+    private final JdbcTemplate jdbcTemplate;
+
+    @Override
+    public Collection<FragmentEntity> prefetchDescendantsOfFragmentEntities(
+            final FetchDescendantsOption fetchDescendantsOption,
+            final Collection<FragmentEntity> proxiedFragmentEntities) {
+
+        if (FetchDescendantsOption.OMIT_DESCENDANTS.equals(fetchDescendantsOption)) {
+            return proxiedFragmentEntities;
+        }
+
+        final List<Long> fragmentEntityIds = proxiedFragmentEntities.stream()
+                .map(FragmentEntity::getId).collect(Collectors.toList());
+
+        final Map<Long, AnchorEntity> anchorEntityPerId = proxiedFragmentEntities.stream()
+                .map(FragmentEntity::getAnchor)
+                .collect(Collectors.toMap(AnchorEntity::getId, anchor -> anchor, (anchor1, anchor2) -> anchor1));
+
+        final int maxDepth = fetchDescendantsOption.equals(FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
+                ? Integer.MAX_VALUE
+                : fetchDescendantsOption.getDepth();
+        return findFragmentEntitiesWithDescendantsByIds(fragmentEntityIds, anchorEntityPerId, maxDepth);
+    }
+
+    private Collection<FragmentEntity> findFragmentEntitiesWithDescendantsByIds(
+            final Collection<Long> fragmentEntityIds,
+            final Map<Long, AnchorEntity> anchorEntityPerId,
+            final int maxDepth) {
+        final String sql
+                = "WITH RECURSIVE parent_search AS ("
+                + "    SELECT id, 0 AS depth "
+                + "    FROM fragment "
+                + "    WHERE id = ANY (?) "
+                + "  UNION "
+                + "    SELECT child.id, depth + 1 "
+                + "    FROM fragment child INNER JOIN parent_search parent ON child.parent_id = parent.id"
+                + "    WHERE depth < ?"
+                + ") "
+                + "SELECT fragment.id, anchor_id AS anchorId, xpath, parent_id AS parentId, "
+                + "       CAST(attributes AS TEXT) AS attributes "
+                + "FROM fragment INNER JOIN parent_search ON fragment.id = parent_search.id";
+
+        final PreparedStatementSetter preparedStatementSetter = preparedStatement -> {
+            final Connection connection = preparedStatement.getConnection();
+            final java.sql.Array idArray = connection.createArrayOf("bigint", fragmentEntityIds.toArray());
+            preparedStatement.setArray(1, idArray);
+            preparedStatement.setInt(2, maxDepth);
+        };
+
+        final RowMapper<FragmentEntity> fragmentEntityRowMapper = (resultSet, rowNum) -> {
+            final FragmentEntity fragmentEntity = new FragmentEntity();
+            fragmentEntity.setId(resultSet.getLong("id"));
+            fragmentEntity.setXpath(resultSet.getString("xpath"));
+            fragmentEntity.setParentId(resultSet.getLong("parentId"));
+            fragmentEntity.setAttributes(resultSet.getString("attributes"));
+            fragmentEntity.setAnchor(anchorEntityPerId.get(resultSet.getLong("anchorId")));
+            fragmentEntity.setChildFragments(new HashSet<>());
+            return fragmentEntity;
+        };
+
+        final Map<Long, FragmentEntity> fragmentEntityPerId;
+        try (final Stream<FragmentEntity> fragmentEntityStream = jdbcTemplate.queryForStream(sql,
+                preparedStatementSetter, fragmentEntityRowMapper)) {
+            fragmentEntityPerId = fragmentEntityStream.collect(
+                    Collectors.toMap(FragmentEntity::getId, Function.identity()));
+        }
+        return reuniteChildrenWithTheirParents(fragmentEntityPerId);
+    }
+
+    private static Collection<FragmentEntity> reuniteChildrenWithTheirParents(
+            final Map<Long, FragmentEntity> fragmentEntityPerId) {
+        final Collection<FragmentEntity> fragmentEntitiesWithoutParent = new HashSet<>();
+        for (final FragmentEntity fragmentEntity : fragmentEntityPerId.values()) {
+            final FragmentEntity parentFragmentEntity = fragmentEntityPerId.get(fragmentEntity.getParentId());
+            if (parentFragmentEntity == null) {
+                fragmentEntitiesWithoutParent.add(fragmentEntity);
+            } else {
+                parentFragmentEntity.getChildFragments().add(fragmentEntity);
+            }
+        }
+        return fragmentEntitiesWithoutParent;
+    }
+
+}
index 82c422f..03de95e 100755 (executable)
@@ -29,7 +29,6 @@ import java.util.Optional;
 import org.onap.cps.spi.entities.AnchorEntity;\r
 import org.onap.cps.spi.entities.DataspaceEntity;\r
 import org.onap.cps.spi.entities.FragmentEntity;\r
-import org.onap.cps.spi.entities.FragmentExtract;\r
 import org.onap.cps.spi.exceptions.DataNodeNotFoundException;\r
 import org.springframework.data.jpa.repository.JpaRepository;\r
 import org.springframework.data.jpa.repository.Modifying;\r
@@ -38,7 +37,8 @@ import org.springframework.data.repository.query.Param;
 import org.springframework.stereotype.Repository;\r
 \r
 @Repository\r
-public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery {\r
+public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>, FragmentRepositoryCpsPathQuery,\r
+        FragmentPrefetchRepository {\r
 \r
     Optional<FragmentEntity> findByAnchorAndXpath(AnchorEntity anchorEntity, String xpath);\r
 \r
@@ -47,7 +47,10 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
             new DataNodeNotFoundException(anchorEntity.getDataspace().getName(), anchorEntity.getName(), xpath));\r
     }\r
 \r
-    List<FragmentEntity> findByAnchorIdAndXpathIn(long anchorId, String[] xpaths);\r
+    @Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId AND xpath = ANY (:xpaths)",\r
+            nativeQuery = true)\r
+    List<FragmentEntity> findByAnchorIdAndXpathIn(@Param("anchorId") long anchorId,\r
+                                                  @Param("xpaths") String[] xpaths);\r
 \r
     default List<FragmentEntity> findByAnchorAndXpathIn(final AnchorEntity anchorEntity,\r
                                                         final Collection<String> xpaths) {\r
@@ -66,8 +69,8 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
 \r
     boolean existsByAnchorId(long anchorId);\r
 \r
-    @Query("SELECT f FROM FragmentEntity f WHERE anchor = :anchor")\r
-    List<FragmentExtract> findAllExtractsByAnchor(@Param("anchor") AnchorEntity anchorEntity);\r
+    @Query(value = "SELECT * FROM fragment WHERE anchor_id = :anchorId LIMIT 1", nativeQuery = true)\r
+    Optional<FragmentEntity> findOneByAnchorId(@Param("anchorId") long anchorId);\r
 \r
     @Modifying\r
     @Query(value = "DELETE FROM fragment WHERE anchor_id = ANY (:anchorIds)", nativeQuery = true)\r
@@ -111,48 +114,4 @@ public interface FragmentRepository extends JpaRepository<FragmentEntity, Long>,
     @Query("SELECT xpath FROM FragmentEntity WHERE anchor = :anchor AND parentId IS NULL")\r
     List<String> findAllXpathByAnchorAndParentIdIsNull(@Param("anchor") AnchorEntity anchorEntity);\r
 \r
-    @Query(value\r
-        = "WITH RECURSIVE parent_search AS ("\r
-        + "  SELECT id, 0 AS depth "\r
-        + "    FROM fragment "\r
-        + "   WHERE anchor_id = :anchorId AND xpath = ANY (:xpaths) "\r
-        + "   UNION "\r
-        + "  SELECT c.id, depth + 1 "\r
-        + "    FROM fragment c INNER JOIN parent_search p ON c.parent_id = p.id"\r
-        + "   WHERE depth < (SELECT CASE WHEN :maxDepth = -1 THEN " + Integer.MAX_VALUE + " ELSE :maxDepth END) "\r
-        + ") "\r
-        + "SELECT f.id, anchor_id AS anchorId, xpath, f.parent_id AS parentId, CAST(attributes AS TEXT) AS attributes "\r
-        + "FROM fragment f INNER JOIN parent_search p ON f.id = p.id",\r
-        nativeQuery = true\r
-    )\r
-    List<FragmentExtract> findExtractsWithDescendants(@Param("anchorId") long anchorId,\r
-                                                      @Param("xpaths") String[] xpaths,\r
-                                                      @Param("maxDepth") int maxDepth);\r
-\r
-    default List<FragmentExtract> findExtractsWithDescendants(final long anchorId, final Collection<String> xpaths,\r
-                                                              final int maxDepth) {\r
-        return findExtractsWithDescendants(anchorId, xpaths.toArray(new String[0]), maxDepth);\r
-    }\r
-\r
-    @Query(value\r
-        = "WITH RECURSIVE parent_search AS ("\r
-        + "  SELECT id, 0 AS depth "\r
-        + "    FROM fragment "\r
-        + "   WHERE id = ANY (:ids) "\r
-        + "   UNION "\r
-        + "  SELECT c.id, depth + 1 "\r
-        + "    FROM fragment c INNER JOIN parent_search p ON c.parent_id = p.id"\r
-        + "   WHERE depth < (SELECT CASE WHEN :maxDepth = -1 THEN " + Integer.MAX_VALUE + " ELSE :maxDepth END) "\r
-        + ") "\r
-        + "SELECT f.id, anchor_id AS anchorId, xpath, f.parent_id AS parentId, CAST(attributes AS TEXT) AS attributes "\r
-        + "FROM fragment f INNER JOIN parent_search p ON f.id = p.id",\r
-        nativeQuery = true\r
-    )\r
-    List<FragmentExtract> findExtractsWithDescendantsByIds(@Param("ids") long[] ids,\r
-                                                           @Param("maxDepth") int maxDepth);\r
-\r
-    default List<FragmentExtract> findExtractsWithDescendantsByIds(final Collection<Long> ids, final int maxDepth) {\r
-        return findExtractsWithDescendantsByIds(ids.stream().mapToLong(id -> id).toArray(), maxDepth);\r
-    }\r
-\r
 }\r
index e8921b3..cb554fa 100644 (file)
@@ -26,7 +26,7 @@ import org.onap.cps.spi.FetchDescendantsOption
 import org.onap.cps.spi.entities.AnchorEntity
 import org.onap.cps.spi.entities.DataspaceEntity
 import org.onap.cps.spi.entities.FragmentEntity
-import org.onap.cps.spi.entities.FragmentExtract
+
 import org.onap.cps.spi.exceptions.ConcurrencyException
 import org.onap.cps.spi.exceptions.DataValidationException
 import org.onap.cps.spi.model.DataNode
@@ -55,6 +55,7 @@ class CpsDataPersistenceServiceSpec extends Specification {
 
     def setup() {
         mockAnchorRepository.getByDataspaceAndName(_, _) >> anchorEntity
+        mockFragmentRepository.prefetchDescendantsOfFragmentEntities(_, _) >> { fetchDescendantsOption, fragmentEntities -> fragmentEntities }
     }
 
     def 'Storing data nodes individually when batch operation fails'(){
@@ -93,20 +94,20 @@ class CpsDataPersistenceServiceSpec extends Specification {
 
     def 'Batch update data node leaves and descendants: #scenario'(){
         given: 'the fragment repository returns fragment entities related to the xpath inputs'
-            mockFragmentRepository.findExtractsWithDescendants(_, [] as Set, _) >> []
-            mockFragmentRepository.findExtractsWithDescendants(_, ['/test/xpath'] as Set, _) >> [
-                    mockFragmentExtract(1, null, 123, '/test/xpath', "{\"id\":\"testId1\"}")
+            mockFragmentRepository.findByAnchorAndXpathIn(_, [] as Set) >> []
+            mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath'] as Set) >> [
+                    new FragmentEntity(1, '/test/xpath', null, "{\"id\":\"testId\"}", anchorEntity, [] as Set)
             ]
-            mockFragmentRepository.findExtractsWithDescendants(123, ['/test/xpath1', '/test/xpath2'] as Set, _) >> [
-                    mockFragmentExtract(1, null, 123, '/test/xpath1', "{\"id\":\"testId1\"}"),
-                    mockFragmentExtract(2, null, 123, '/test/xpath2', "{\"id\":\"testId1\"}")
+            mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath1', '/test/xpath2'] as Set) >> [
+                    new FragmentEntity(1, '/test/xpath1', null, "{\"id\":\"testId1\"}", anchorEntity, [] as Set),
+                    new FragmentEntity(2, '/test/xpath2', null, "{\"id\":\"testId2\"}", anchorEntity, [] as Set)
             ]
         when: 'replace data node tree'
             objectUnderTest.batchUpdateDataLeaves('dataspaceName', 'anchorName',
                     dataNodes.stream().collect(Collectors.toMap(DataNode::getXpath, DataNode::getLeaves)))
         then: 'call fragment repository save all method'
             1 * mockFragmentRepository.saveAll({fragmentEntities ->
-                assert fragmentEntities as List == expectedFragmentEntities
+                assert fragmentEntities.sort() == expectedFragmentEntities.sort()
                 assert fragmentEntities.size() == expectedSize
             })
         where: 'the following Data Type is passed'
@@ -172,9 +173,9 @@ class CpsDataPersistenceServiceSpec extends Specification {
 
     def 'Retrieving multiple data nodes.'() {
         given: 'fragment repository returns a collection of fragments'
-            mockFragmentRepository.findExtractsWithDescendants(123, ['/xpath1', '/xpath2'] as Set, _) >> [
-                mockFragmentExtract(1, null, 123, '/xpath1', null),
-                mockFragmentExtract(2, null, 123, '/xpath2', null)
+            mockFragmentRepository.findByAnchorAndXpathIn(anchorEntity, ['/xpath1', '/xpath2'] as Set) >> [
+                new FragmentEntity(1, '/xpath1', null, null, anchorEntity, [] as Set),
+                new FragmentEntity(2, '/xpath2', null, null, anchorEntity, [] as Set)
             ]
         when: 'getting data nodes for 2 xpaths'
             def result = objectUnderTest.getDataNodesForMultipleXpaths('some-dataspace', 'some-anchor', ['/xpath1', '/xpath2'], FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS)
@@ -207,9 +208,9 @@ class CpsDataPersistenceServiceSpec extends Specification {
 
     def 'Replace data node and descendants: #scenario'(){
         given: 'the fragment repository returns fragment entities related to the xpath inputs'
-            mockFragmentRepository.findExtractsWithDescendants(_, [] as Set, _) >> []
-            mockFragmentRepository.findExtractsWithDescendants(_, ['/test/xpath'] as Set, _) >> [
-                mockFragmentExtract(1, null, 123, '/test/xpath', null)
+            mockFragmentRepository.findByAnchorAndXpathIn(_, [] as Set) >> []
+            mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath'] as Set) >> [
+                new FragmentEntity(1, '/test/xpath', null, '{"id":"testId"}', anchorEntity, [] as Set)
             ]
         when: 'replace data node tree'
             objectUnderTest.updateDataNodesAndDescendants('dataspaceName', 'anchorName', dataNodes)
@@ -223,9 +224,9 @@ class CpsDataPersistenceServiceSpec extends Specification {
 
     def 'Replace data nodes and descendants'() {
         given: 'the fragment repository returns fragment entities related to the xpath inputs'
-            mockFragmentRepository.findExtractsWithDescendants(_, ['/test/xpath1', '/test/xpath2'] as Set, _) >> [
-                mockFragmentExtract(1, null, 123, '/test/xpath1', null),
-                mockFragmentExtract(2, null, 123, '/test/xpath2', null)
+            mockFragmentRepository.findByAnchorAndXpathIn(_, ['/test/xpath1', '/test/xpath2'] as Set) >> [
+                new FragmentEntity(1, '/test/xpath1', null, null, anchorEntity, [] as Set),
+                new FragmentEntity(2, '/test/xpath2', null, null, anchorEntity, [] as Set)
             ]
         and: 'some data nodes with descendants'
             def dataNode1 = new DataNode(xpath: '/test/xpath1', leaves: ['id': 'testId1'], childDataNodes: [new DataNode(xpath: '/test/xpath1/child', leaves: ['id': 'childTestId1'])])
@@ -253,38 +254,27 @@ class CpsDataPersistenceServiceSpec extends Specification {
 
     def createDataNodesAndMockRepositoryMethodSupportingThem(Map<String, String> xpathToScenarioMap) {
         def dataNodes = []
-        def fragmentExtracts = []
+        def fragmentEntities = []
         def fragmentId = 1
         xpathToScenarioMap.each {
             def xpath = it.key
             def scenario = it.value
             def dataNode = new DataNodeBuilder().withXpath(xpath).build()
             dataNodes.add(dataNode)
-            def fragmentExtract = mockFragmentExtract(fragmentId, null, 123, xpath, null)
-            fragmentExtracts.add(fragmentExtract)
             def fragmentEntity = new FragmentEntity(id: fragmentId, anchor: anchorEntity, xpath: xpath, childFragments: [])
+            fragmentEntities.add(fragmentEntity)
             if ('EXCEPTION' == scenario) {
                 mockFragmentRepository.save(fragmentEntity) >> { throw new StaleStateException("concurrent updates") }
             }
             fragmentId++
         }
-        mockFragmentRepository.findExtractsWithDescendants(_, xpathToScenarioMap.keySet(), _) >> fragmentExtracts
+        mockFragmentRepository.findByAnchorAndXpathIn(_, xpathToScenarioMap.keySet()) >> fragmentEntities
         return dataNodes
     }
 
     def mockFragmentWithJson(json) {
-        def fragmentExtract = mockFragmentExtract(456, null, 123, '/parent-01', json)
-        mockFragmentRepository.findExtractsWithDescendants(123, ['/parent-01'] as Set, _) >> [fragmentExtract]
-    }
-
-    def mockFragmentExtract(id, parentId, anchorId, xpath, attributes) {
-        def fragmentExtract = Mock(FragmentExtract)
-        fragmentExtract.getId() >> id
-        fragmentExtract.getParentId() >> parentId
-        fragmentExtract.getAnchorId() >> anchorId
-        fragmentExtract.getXpath() >> xpath
-        fragmentExtract.getAttributes() >> attributes
-        return fragmentExtract
+        def fragmentEntity = new FragmentEntity(456, '/parent-01', null, json, anchorEntity, [] as Set)
+        mockFragmentRepository.findByAnchorAndXpathIn(_, ['/parent-01'] as Set) >> [fragmentEntity]
     }
 
 }