Merge pull request #7548 from thingsboard/feature/findRelationsRecursively

Improvement to the recursive relations query.
This commit is contained in:
Andrew Shvayka 2022-11-04 15:36:29 +02:00 committed by GitHub
commit a6a992f0dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 485 additions and 46 deletions

View File

@ -310,7 +310,9 @@ sql:
ttl: "${SQL_TTL_AUDIT_LOGS_SECS:0}" # Disabled by default. Accuracy of the cleanup depends on the sql.audit_logs.partition_size
checking_interval_ms: "${SQL_TTL_AUDIT_LOGS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day
relations:
max_level: "${SQL_RELATIONS_MAX_LEVEL:50}" # //This value has to be reasonable small to prevent infinite recursion as early as possible
max_level: "${SQL_RELATIONS_MAX_LEVEL:50}" # This value has to be reasonable small to prevent infinite recursion as early as possible
pool_size: "${SQL_RELATIONS_POOL_SIZE:4}" # This value has to be reasonable small to prevent relation query blocking all other DB calls
query_timeout: "${SQL_RELATIONS_QUERY_TIMEOUT_SEC:20}" # This value has to be reasonable small to prevent relation query blocking all other DB calls
# Actor system parameters
actors:

View File

@ -15,6 +15,10 @@
*/
package org.thingsboard.common.util;
import com.google.common.util.concurrent.MoreExecutors;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
@ -47,4 +51,5 @@ public class ThingsBoardExecutors {
public static ExecutorService newWorkStealingPool(int parallelism, Class clazz) {
return newWorkStealingPool(parallelism, clazz.getSimpleName());
}
}

View File

@ -20,7 +20,10 @@ import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.dao.ConcurrencyFailureException;
@ -28,6 +31,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cache.TbTransactionalCache;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
@ -44,13 +48,22 @@ import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.dao.sql.relation.JpaRelationQueryExecutorService;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import static org.thingsboard.server.dao.service.Validator.validateId;
@ -67,15 +80,34 @@ public class BaseRelationService implements RelationService {
private final TbTransactionalCache<RelationCacheKey, RelationCacheValue> cache;
private final ApplicationEventPublisher eventPublisher;
private final JpaExecutorService executor;
private final JpaRelationQueryExecutorService relationsExecutor;
protected ScheduledExecutorService timeoutExecutorService;
@Value("${sql.relations.query_timeout:20}")
private Integer relationQueryTimeout;
public BaseRelationService(RelationDao relationDao, @Lazy EntityService entityService,
TbTransactionalCache<RelationCacheKey, RelationCacheValue> cache,
ApplicationEventPublisher eventPublisher, JpaExecutorService executor) {
ApplicationEventPublisher eventPublisher, JpaExecutorService executor,
JpaRelationQueryExecutorService relationsExecutor) {
this.relationDao = relationDao;
this.entityService = entityService;
this.cache = cache;
this.eventPublisher = eventPublisher;
this.executor = executor;
this.relationsExecutor = relationsExecutor;
}
@PostConstruct
public void init() {
timeoutExecutorService = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("relations-query-timeout"));
}
@PreDestroy
public void destroy() {
if (timeoutExecutorService != null) {
timeoutExecutorService.shutdownNow();
}
}
@TransactionalEventListener(classes = EntityRelationEvent.class)
@ -375,7 +407,6 @@ public class BaseRelationService implements RelationService {
@Override
public ListenableFuture<List<EntityRelation>> findByQuery(TenantId tenantId, EntityRelationsQuery query) {
//boolean fetchLastLevelOnly = true;
log.trace("Executing findByQuery [{}]", query);
RelationsSearchParameters params = query.getParameters();
final List<RelationEntityTypeFilter> filters = query.getFilters();
@ -386,7 +417,8 @@ public class BaseRelationService implements RelationService {
int maxLvl = params.getMaxLevel() > 0 ? params.getMaxLevel() : Integer.MAX_VALUE;
try {
ListenableFuture<Set<EntityRelation>> relationSet = findRelationsRecursively(tenantId, params.getEntityId(), params.getDirection(), params.getRelationTypeGroup(), maxLvl, params.isFetchLastLevelOnly(), new ConcurrentHashMap<>());
ListenableFuture<Set<EntityRelation>> relationSet = findRelationsRecursively(tenantId, params.getEntityId(), params.getDirection(),
params.getRelationTypeGroup(), maxLvl, params.isFetchLastLevelOnly(), new ConcurrentHashMap<>());
return Futures.transform(relationSet, input -> {
List<EntityRelation> relations = new ArrayList<>();
if (filters == null || filters.isEmpty()) {
@ -510,53 +542,89 @@ public class BaseRelationService implements RelationService {
}
}
@RequiredArgsConstructor
private static class RelationQueueCtx {
final SettableFuture<Set<EntityRelation>> future = SettableFuture.create();
final Set<EntityRelation> result = ConcurrentHashMap.newKeySet();
final Queue<RelationTask> tasks = new ConcurrentLinkedQueue<>();
final TenantId tenantId;
final EntitySearchDirection direction;
final RelationTypeGroup relationTypeGroup;
final boolean fetchLastLevelOnly;
final int maxLvl;
final ConcurrentHashMap<EntityId, Boolean> uniqueMap;
}
@RequiredArgsConstructor
private static class RelationTask {
private final int currentLvl;
private final EntityId root;
private final List<EntityRelation> prevRelations;
}
private void processQueue(RelationQueueCtx ctx) {
RelationTask task = ctx.tasks.poll();
while (task != null) {
List<EntityRelation> relations = findRelations(ctx.tenantId, task.root, ctx.direction, ctx.relationTypeGroup);
Map<EntityId, List<EntityRelation>> newChildrenRelations = new HashMap<>();
for (EntityRelation childRelation : relations) {
log.trace("Found Relation: {}", childRelation);
EntityId childId = ctx.direction == EntitySearchDirection.FROM ? childRelation.getTo() : childRelation.getFrom();
if (ctx.uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) {
log.trace("Adding Relation: {}", childId);
newChildrenRelations.put(childId, new ArrayList<>());
}
if (ctx.fetchLastLevelOnly) {
var list = newChildrenRelations.get(childId);
if (list != null) {
list.add(childRelation);
}
}
}
if (ctx.fetchLastLevelOnly) {
if (relations.isEmpty()) {
ctx.result.addAll(task.prevRelations);
} else if (task.currentLvl == ctx.maxLvl) {
ctx.result.addAll(relations);
}
} else {
ctx.result.addAll(relations);
}
var finalTask = task;
newChildrenRelations.forEach((child, childRelations) -> {
var newLvl = finalTask.currentLvl + 1;
if (newLvl <= ctx.maxLvl)
ctx.tasks.add(new RelationTask(newLvl, child, childRelations));
});
task = ctx.tasks.poll();
}
ctx.future.set(ctx.result);
}
private ListenableFuture<Set<EntityRelation>> findRelationsRecursively(final TenantId tenantId, final EntityId rootId, final EntitySearchDirection direction,
RelationTypeGroup relationTypeGroup, int lvl, boolean fetchLastLevelOnly,
final ConcurrentHashMap<EntityId, Boolean> uniqueMap) throws Exception {
final ConcurrentHashMap<EntityId, Boolean> uniqueMap) {
if (lvl == 0) {
return Futures.immediateFuture(Collections.emptySet());
}
lvl--;
//TODO: try to remove this blocking operation
Set<EntityRelation> children = new HashSet<>(findRelations(tenantId, rootId, direction, relationTypeGroup).get());
Set<EntityId> childrenIds = new HashSet<>();
for (EntityRelation childRelation : children) {
log.trace("Found Relation: {}", childRelation);
EntityId childId;
if (direction == EntitySearchDirection.FROM) {
childId = childRelation.getTo();
} else {
childId = childRelation.getFrom();
}
if (uniqueMap.putIfAbsent(childId, Boolean.TRUE) == null) {
log.trace("Adding Relation: {}", childId);
if (childrenIds.add(childId)) {
log.trace("Added Relation: {}", childId);
}
}
}
List<ListenableFuture<Set<EntityRelation>>> futures = new ArrayList<>();
for (EntityId entityId : childrenIds) {
futures.add(findRelationsRecursively(tenantId, entityId, direction, relationTypeGroup, lvl, fetchLastLevelOnly, uniqueMap));
}
//TODO: try to remove this blocking operation
List<Set<EntityRelation>> relations = Futures.successfulAsList(futures).get();
if (fetchLastLevelOnly && lvl > 0) {
children.clear();
}
relations.forEach(r -> r.forEach(children::add));
return Futures.immediateFuture(children);
var relationQueueCtx = new RelationQueueCtx(tenantId, direction, relationTypeGroup, fetchLastLevelOnly, lvl, uniqueMap);
relationQueueCtx.tasks.add(new RelationTask(1, rootId, Collections.emptyList()));
relationsExecutor.submit(() -> processQueue(relationQueueCtx));
return Futures.withTimeout(relationQueueCtx.future, relationQueryTimeout, TimeUnit.SECONDS, timeoutExecutorService);
}
private ListenableFuture<List<EntityRelation>> findRelations(final TenantId tenantId, final EntityId rootId, final EntitySearchDirection direction, RelationTypeGroup relationTypeGroup) {
ListenableFuture<List<EntityRelation>> relations;
private List<EntityRelation> findRelations(final TenantId tenantId, final EntityId rootId, final EntitySearchDirection direction, RelationTypeGroup relationTypeGroup) {
List<EntityRelation> relations;
if (relationTypeGroup == null) {
relationTypeGroup = RelationTypeGroup.COMMON;
}
if (direction == EntitySearchDirection.FROM) {
relations = findByFromAsync(tenantId, rootId, relationTypeGroup);
relations = findByFrom(tenantId, rootId, relationTypeGroup);
} else {
relations = findByToAsync(tenantId, rootId, relationTypeGroup);
relations = findByTo(tenantId, rootId, relationTypeGroup);
}
return relations;
}

View File

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* 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.
*/
package org.thingsboard.server.dao.sql.relation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.common.util.AbstractListeningExecutor;
@Component
public class JpaRelationQueryExecutorService extends AbstractListeningExecutor {
@Value("${sql.relations.pool_size:4}")
private int poolSize;
@Override
protected int getThreadPollSize() {
return poolSize;
}
}

View File

@ -35,6 +35,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
@ -80,7 +81,7 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest {
Assert.assertTrue(relationService.deleteRelationAsync(SYSTEM_TENANT_ID, relationA).get());
Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON));
Assert.assertFalse(relationService.checkRelation(SYSTEM_TENANT_ID, parentId, childId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON));
Assert.assertTrue(relationService.checkRelation(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON));
@ -171,8 +172,8 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest {
Assert.assertEquals(0, relations.size());
}
private Boolean saveRelation(EntityRelation relationA1) throws ExecutionException, InterruptedException {
return relationService.saveRelationAsync(SYSTEM_TENANT_ID, relationA1).get();
private Boolean saveRelation(EntityRelation relationA1) {
return relationService.saveRelation(SYSTEM_TENANT_ID, relationA1);
}
@Test
@ -194,9 +195,6 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest {
saveRelation(relationB1);
saveRelation(relationB2);
// Data propagation to views is async
Thread.sleep(3000);
List<EntityRelation> relations = relationService.findByTo(SYSTEM_TENANT_ID, childA, RelationTypeGroup.COMMON);
Assert.assertEquals(2, relations.size());
for (EntityRelation relation : relations) {
@ -288,6 +286,53 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest {
Assert.assertTrue(relations.contains(relationBC));
}
@Test
public void testRecursiveRelationDepth() throws ExecutionException, InterruptedException {
int maxLevel = 1000;
AssetId root = new AssetId(Uuids.timeBased());
AssetId left = new AssetId(Uuids.timeBased());
AssetId right = new AssetId(Uuids.timeBased());
List<EntityRelation> expected = new ArrayList<>();
EntityRelation relationAB = new EntityRelation(root, left, EntityRelation.CONTAINS_TYPE);
EntityRelation relationBC = new EntityRelation(root, right, EntityRelation.CONTAINS_TYPE);
saveRelation(relationAB);
expected.add(relationAB);
saveRelation(relationBC);
expected.add(relationBC);
for (int i = 0; i < maxLevel; i++) {
var newLeft = new AssetId(Uuids.timeBased());
var newRight = new AssetId(Uuids.timeBased());
EntityRelation relationLeft = new EntityRelation(left, newLeft, EntityRelation.CONTAINS_TYPE);
EntityRelation relationRight = new EntityRelation(right, newRight, EntityRelation.CONTAINS_TYPE);
saveRelation(relationLeft);
expected.add(relationLeft);
saveRelation(relationRight);
expected.add(relationRight);
left = newLeft;
right = newRight;
}
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(root, EntitySearchDirection.FROM, -1, false));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
List<EntityRelation> relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(expected.size(), relations.size());
for(EntityRelation r : expected){
Assert.assertTrue(relations.contains(r));
}
//Test from cache
relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(expected.size(), relations.size());
for(EntityRelation r : expected){
Assert.assertTrue(relations.contains(r));
}
}
@Test(expected = DataValidationException.class)
public void testSaveRelationWithEmptyFrom() throws ExecutionException, InterruptedException {
@ -312,4 +357,290 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest {
relation.setTo(new AssetId(Uuids.timeBased()));
Assert.assertTrue(saveRelation(relation));
}
@Test
public void testFindByQueryFetchLastOnlyTreeLike() throws Exception {
// A -> B
// A -> C
// C -> D
// C -> E
AssetId assetA = new AssetId(Uuids.timeBased());
AssetId assetB = new AssetId(Uuids.timeBased());
AssetId assetC = new AssetId(Uuids.timeBased());
AssetId assetD = new AssetId(Uuids.timeBased());
AssetId assetE = new AssetId(Uuids.timeBased());
EntityRelation relationA = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE);
EntityRelation relationB = new EntityRelation(assetA, assetC, EntityRelation.CONTAINS_TYPE);
EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE);
EntityRelation relationD = new EntityRelation(assetC, assetE, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA);
saveRelation(relationB);
saveRelation(relationC);
saveRelation(relationD);
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, true));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
List<EntityRelation> relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(3, relations.size());
Assert.assertTrue(relations.contains(relationA));
Assert.assertTrue(relations.contains(relationC));
Assert.assertTrue(relations.contains(relationD));
Assert.assertFalse(relations.contains(relationB));
//Test from cache
relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertTrue(relations.contains(relationA));
Assert.assertTrue(relations.contains(relationC));
Assert.assertTrue(relations.contains(relationD));
Assert.assertFalse(relations.contains(relationB));
}
@Test
public void testFindByQueryFetchLastOnlySingleLinked() throws Exception {
// A -> B -> C -> D
AssetId assetA = new AssetId(Uuids.timeBased());
AssetId assetB = new AssetId(Uuids.timeBased());
AssetId assetC = new AssetId(Uuids.timeBased());
AssetId assetD = new AssetId(Uuids.timeBased());
EntityRelation relationA = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE);
EntityRelation relationB = new EntityRelation(assetB, assetC, EntityRelation.CONTAINS_TYPE);
EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA);
saveRelation(relationB);
saveRelation(relationC);
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, true));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
List<EntityRelation> relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(1, relations.size());
Assert.assertTrue(relations.contains(relationC));
Assert.assertFalse(relations.contains(relationA));
Assert.assertFalse(relations.contains(relationB));
//Test from cache
relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertTrue(relations.contains(relationC));
Assert.assertFalse(relations.contains(relationA));
Assert.assertFalse(relations.contains(relationB));
}
@Test
public void testFindByQueryFetchLastOnlyTreeLikeWithMaxLvl() throws Exception {
// A -> B A
// A -> C B
// C -> D C
// C -> E D
// D -> F E
// D -> G F
AssetId assetA = new AssetId(Uuids.timeBased());
AssetId assetB = new AssetId(Uuids.timeBased());
AssetId assetC = new AssetId(Uuids.timeBased());
AssetId assetD = new AssetId(Uuids.timeBased());
AssetId assetE = new AssetId(Uuids.timeBased());
AssetId assetF = new AssetId(Uuids.timeBased());
AssetId assetG = new AssetId(Uuids.timeBased());
EntityRelation relationA = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE);
EntityRelation relationB = new EntityRelation(assetA, assetC, EntityRelation.CONTAINS_TYPE);
EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE);
EntityRelation relationD = new EntityRelation(assetC, assetE, EntityRelation.CONTAINS_TYPE);
EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE);
EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA);
saveRelation(relationB);
saveRelation(relationC);
saveRelation(relationD);
saveRelation(relationE);
saveRelation(relationF);
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, 2, true));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
List<EntityRelation> relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(3, relations.size());
Assert.assertTrue(relations.contains(relationA));
Assert.assertTrue(relations.contains(relationC));
Assert.assertTrue(relations.contains(relationD));
Assert.assertFalse(relations.contains(relationB));
Assert.assertFalse(relations.contains(relationE));
Assert.assertFalse(relations.contains(relationF));
//Test from cache
relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertTrue(relations.contains(relationA));
Assert.assertTrue(relations.contains(relationC));
Assert.assertTrue(relations.contains(relationD));
Assert.assertFalse(relations.contains(relationB));
Assert.assertFalse(relations.contains(relationE));
Assert.assertFalse(relations.contains(relationF));
}
@Test
public void testFindByQueryTreeLikeWithMaxLvl() throws Exception {
// A -> B A
// A -> C B
// C -> D C
// C -> E D
// D -> F E
// D -> G F
AssetId assetA = new AssetId(Uuids.timeBased());
AssetId assetB = new AssetId(Uuids.timeBased());
AssetId assetC = new AssetId(Uuids.timeBased());
AssetId assetD = new AssetId(Uuids.timeBased());
AssetId assetE = new AssetId(Uuids.timeBased());
AssetId assetF = new AssetId(Uuids.timeBased());
AssetId assetG = new AssetId(Uuids.timeBased());
EntityRelation relationA = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE);
EntityRelation relationB = new EntityRelation(assetA, assetC, EntityRelation.CONTAINS_TYPE);
EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE);
EntityRelation relationD = new EntityRelation(assetC, assetE, EntityRelation.CONTAINS_TYPE);
EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE);
EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA);
saveRelation(relationB);
saveRelation(relationC);
saveRelation(relationD);
saveRelation(relationE);
saveRelation(relationF);
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, 2, false));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
List<EntityRelation> relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(4, relations.size());
Assert.assertTrue(relations.contains(relationA));
Assert.assertTrue(relations.contains(relationB));
Assert.assertTrue(relations.contains(relationC));
Assert.assertTrue(relations.contains(relationD));
Assert.assertFalse(relations.contains(relationE));
Assert.assertFalse(relations.contains(relationF));
//Test from cache
relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertTrue(relations.contains(relationA));
Assert.assertTrue(relations.contains(relationB));
Assert.assertTrue(relations.contains(relationC));
Assert.assertTrue(relations.contains(relationD));
Assert.assertFalse(relations.contains(relationE));
Assert.assertFalse(relations.contains(relationF));
}
@Test
public void testFindByQueryTreeLikeWithUnlimLvl() throws Exception {
// A -> B A
// A -> C B
// C -> D C
// C -> E D
// D -> F E
// D -> G F
AssetId assetA = new AssetId(Uuids.timeBased());
AssetId assetB = new AssetId(Uuids.timeBased());
AssetId assetC = new AssetId(Uuids.timeBased());
AssetId assetD = new AssetId(Uuids.timeBased());
AssetId assetE = new AssetId(Uuids.timeBased());
AssetId assetF = new AssetId(Uuids.timeBased());
AssetId assetG = new AssetId(Uuids.timeBased());
EntityRelation relationA = new EntityRelation(assetA, assetB, EntityRelation.CONTAINS_TYPE);
EntityRelation relationB = new EntityRelation(assetA, assetC, EntityRelation.CONTAINS_TYPE);
EntityRelation relationC = new EntityRelation(assetC, assetD, EntityRelation.CONTAINS_TYPE);
EntityRelation relationD = new EntityRelation(assetC, assetE, EntityRelation.CONTAINS_TYPE);
EntityRelation relationE = new EntityRelation(assetD, assetF, EntityRelation.CONTAINS_TYPE);
EntityRelation relationF = new EntityRelation(assetD, assetG, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA);
saveRelation(relationB);
saveRelation(relationC);
saveRelation(relationD);
saveRelation(relationE);
saveRelation(relationF);
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(assetA, EntitySearchDirection.FROM, -1, false));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
List<EntityRelation> relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(6, relations.size());
Assert.assertTrue(relations.contains(relationA));
Assert.assertTrue(relations.contains(relationB));
Assert.assertTrue(relations.contains(relationC));
Assert.assertTrue(relations.contains(relationD));
Assert.assertTrue(relations.contains(relationE));
Assert.assertTrue(relations.contains(relationF));
//Test from cache
relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertTrue(relations.contains(relationA));
Assert.assertTrue(relations.contains(relationB));
Assert.assertTrue(relations.contains(relationC));
Assert.assertTrue(relations.contains(relationD));
Assert.assertTrue(relations.contains(relationE));
Assert.assertTrue(relations.contains(relationF));
}
@Test
public void testFindByQueryLargeHierarchyFetchAllWithUnlimLvl() throws Exception {
AssetId rootAsset = new AssetId(Uuids.timeBased());
final int hierarchyLvl = 10;
List<EntityRelation> expectedRelations = new LinkedList<>();
createAssetRelationsRecursively(rootAsset, hierarchyLvl, expectedRelations, false);
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(rootAsset, EntitySearchDirection.FROM, -1, false));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
List<EntityRelation> relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(expectedRelations.size(), relations.size());
Assert.assertTrue(relations.containsAll(expectedRelations));
}
@Test
public void testFindByQueryLargeHierarchyFetchLastOnlyWithUnlimLvl() throws Exception {
AssetId rootAsset = new AssetId(Uuids.timeBased());
final int hierarchyLvl = 10;
List<EntityRelation> expectedRelations = new LinkedList<>();
createAssetRelationsRecursively(rootAsset, hierarchyLvl, expectedRelations, true);
EntityRelationsQuery query = new EntityRelationsQuery();
query.setParameters(new RelationsSearchParameters(rootAsset, EntitySearchDirection.FROM, -1, true));
query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET))));
List<EntityRelation> relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get();
Assert.assertEquals(expectedRelations.size(), relations.size());
Assert.assertTrue(relations.containsAll(expectedRelations));
}
private void createAssetRelationsRecursively(AssetId rootAsset, int lvl, List<EntityRelation> entityRelations, boolean lastLvlOnly) throws Exception {
if (lvl == 0) return;
AssetId firstAsset = new AssetId(Uuids.timeBased());
AssetId secondAsset = new AssetId(Uuids.timeBased());
EntityRelation firstRelation = new EntityRelation(rootAsset, firstAsset, EntityRelation.CONTAINS_TYPE);
EntityRelation secondRelation = new EntityRelation(rootAsset, secondAsset, EntityRelation.CONTAINS_TYPE);
saveRelation(firstRelation);
saveRelation(secondRelation);
if (!lastLvlOnly || lvl == 1) {
entityRelations.add(firstRelation);
entityRelations.add(secondRelation);
}
createAssetRelationsRecursively(firstAsset, lvl - 1, entityRelations, lastLvlOnly);
createAssetRelationsRecursively(secondAsset, lvl - 1, entityRelations, lastLvlOnly);
}
}