Browse Source

Merge pull request #9830 from thingsboard/relation_query_not_option

[Feature Request] - Add "NOT" option in relation queries
pull/10856/head
Andrew Shvayka 2 years ago
committed by GitHub
parent
commit
337d8f0a95
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 63
      application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java
  2. 1
      common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java
  3. 17
      common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java
  4. 21
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  5. 3
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html
  6. 3
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts
  7. 6
      ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts
  8. 1
      ui-ngx/src/app/shared/models/alias.models.ts
  9. 1
      ui-ngx/src/app/shared/models/relation.models.ts
  10. 4
      ui-ngx/src/assets/locale/locale.constant-en_US.json

63
application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java

@ -53,8 +53,12 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.query.KeyFilter;
import org.thingsboard.server.common.data.query.NumericFilterPredicate;
import org.thingsboard.server.common.data.query.StringFilterPredicate;
import org.thingsboard.server.common.data.query.RelationsQueryFilter;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.queue.QueueStats;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.queue.QueueStatsService;
import org.thingsboard.server.dao.service.DaoSqlTest;
@ -423,6 +427,65 @@ public class EntityQueryControllerTest extends AbstractControllerTest {
}
@Test
public void testFindEntityDataByQueryWithNegateParam() throws Exception {
List<Device> devices = new ArrayList<>();
for (int i = 0; i < 97; i++) {
Device device = new Device();
device.setName("Device" + i);
device.setType("default");
device.setLabel("testLabel" + (int) (Math.random() * 1000));
devices.add(doPost("/api/device", device, Device.class));
Thread.sleep(1);
}
Device mainDevice = new Device();
mainDevice.setName("Main device");
mainDevice = doPost("/api/device", mainDevice, Device.class);
for (int i = 0; i < 10; i++) {
EntityRelation relation = createFromRelation(mainDevice, devices.get(i), "CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk());
}
for (int i = 10; i < 97; i++) {
EntityRelation relation = createFromRelation(mainDevice, devices.get(i), "NOT_CONTAINS");
doPost("/api/relation", relation).andExpect(status().isOk());
}
RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(mainDevice.getId());
filter.setDirection(EntitySearchDirection.FROM);
filter.setNegate(true);
filter.setFilters(List.of(new RelationEntityTypeFilter("CONTAINS", List.of(EntityType.DEVICE), false)));
EntityDataSortOrder sortOrder = new EntityDataSortOrder(
new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime"), EntityDataSortOrder.Direction.ASC
);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
List<EntityKey> entityFields = Collections.singletonList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
PageData<EntityData> data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {});
Assert.assertEquals(87, data.getTotalElements());
filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), false)));
query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {});
Assert.assertEquals(10, data.getTotalElements());
filter.setFilters(List.of(new RelationEntityTypeFilter("NOT_CONTAINS", List.of(EntityType.DEVICE), true)));
query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
data = doPostWithTypedResponse("/api/entitiesQuery/find", query, new TypeReference<>() {});
Assert.assertEquals(87, data.getTotalElements());
}
private EntityRelation createFromRelation(Device mainDevice, Device device, String relationType) {
return new EntityRelation(mainDevice.getId(), device.getId(), relationType);
}
@Test
public void testFindEntityDataByQueryWithAttributes() throws Exception {
List<Device> devices = new ArrayList<>();

1
common/data/src/main/java/org/thingsboard/server/common/data/query/RelationsQueryFilter.java

@ -40,5 +40,6 @@ public class RelationsQueryFilter implements EntityFilter {
private List<RelationEntityTypeFilter> filters;
private int maxLevel;
private boolean fetchLastLevelOnly;
private boolean negate;
}

17
common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationEntityTypeFilter.java

@ -16,7 +16,6 @@
package org.thingsboard.server.common.data.relation;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
@ -26,13 +25,27 @@ import java.util.List;
* Created by ashvayka on 02.05.17.
*/
@Data
@AllArgsConstructor
@Schema
public class RelationEntityTypeFilter {
public RelationEntityTypeFilter() {}
public RelationEntityTypeFilter(String relationType, List<EntityType> entityTypes) {
this(relationType, entityTypes, false);
}
public RelationEntityTypeFilter(String relationType, List<EntityType> entityTypes, boolean negate) {
this.relationType = relationType;
this.entityTypes = entityTypes;
this.negate = negate;
}
@Schema(description = "Type of the relation between root entity and other entity (e.g. 'Contains' or 'Manages').", example = "Contains")
private String relationType;
@Schema(description = "Array of entity types to filter the related entities (e.g. 'DEVICE', 'ASSET').")
private List<EntityType> entityTypes;
@Schema(description = "Negate relation type between root entity and other entity.")
private boolean negate;
}

21
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -729,10 +729,16 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
.append(entityFilter.getDirection().equals(EntitySearchDirection.FROM) ? "to" : "from")
.append("_type in (:where_entity_types").append(")");
ctx.addStringListParameter("where_entity_types", Arrays.stream(RELATION_QUERY_ENTITY_TYPES).map(EntityType::name).collect(Collectors.toList()));
}
if (!noConditions && !single) {
whereFilter = new StringBuilder().append("(").append(whereFilter).append(")");
} else {
if (!single) {
whereFilter = new StringBuilder()
.append(entityFilter.isNegate() ? " NOT (" : "(")
.append(whereFilter).append(")");
} else if (entityFilter.isNegate()) {
whereFilter = new StringBuilder()
.append(" NOT (")
.append(whereFilter).append(")");
}
}
if (entityFilter.isFetchLastLevelOnly()) {
@ -768,8 +774,11 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
boolean hasRelationType = !StringUtils.isEmpty(relationType);
if (hasRelationType) {
ctx.addStringParameter("where_relation_type" + entityTypeFilterIdx, relationType);
whereFilter
.append("re.relation_type = :where_relation_type").append(entityTypeFilterIdx);
if (etf.isNegate()) {
whereFilter.append("re.relation_type != :where_relation_type").append(entityTypeFilterIdx);
} else {
whereFilter.append("re.relation_type = :where_relation_type").append(entityTypeFilterIdx);
}
}
if (!whereEntityTypes.isEmpty()) {
if (hasRelationType) {

3
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html

@ -186,6 +186,9 @@
formControlName="maxLevel">
</mat-form-field>
</div>
<mat-slide-toggle formControlName="negate" *ngIf="this.filterFormGroup.get('filters').value.length > 1">
{{ 'relation.negate-all-relations' | translate }}
</mat-slide-toggle>
<div class="mat-caption" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>relation.relation-filters</div>
<tb-relation-filters
[allowedEntityTypes]="allowedEntityTypes"

3
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.ts

@ -197,7 +197,8 @@ export class EntityFilterComponent implements ControlValueAccessor, OnInit, OnDe
rootEntity: [filter ? filter.rootEntity : null, (filter && filter.rootStateEntity) ? [] : [Validators.required]],
direction: [filter ? filter.direction : EntitySearchDirection.FROM, [Validators.required]],
maxLevel: [filter ? filter.maxLevel : 1, []],
fetchLastLevelOnly: [filter ? filter.fetchLastLevelOnly : false, []]
fetchLastLevelOnly: [filter ? filter.fetchLastLevelOnly : false, []],
negate: [filter ? filter.negate : false, []]
});
const rootStateSubscription = this.filterFormGroup.get('rootStateEntity').valueChanges.subscribe((rootStateEntity: boolean) => {
this.filterFormGroup.get('rootEntity').setValidators(rootStateEntity ? [] : [Validators.required]);

6
ui-ngx/src/app/modules/home/components/relation/relation-filters.component.ts

@ -112,7 +112,8 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
public addFilter() {
const filter: RelationEntityTypeFilter = {
relationType: null,
entityTypes: []
entityTypes: [],
negate: false
};
this.relationFiltersFormArray.push(this.createRelationFilterFormGroup(filter));
}
@ -120,7 +121,8 @@ export class RelationFiltersComponent extends PageComponent implements ControlVa
private createRelationFilterFormGroup(filter: RelationEntityTypeFilter): AbstractControl {
return this.fb.group({
relationType: [filter ? filter.relationType : null],
entityTypes: [filter ? filter.entityTypes : []]
entityTypes: [filter ? filter.entityTypes : []],
negate: [filter ? filter.negate : false],
});
}

1
ui-ngx/src/app/shared/models/alias.models.ts

@ -130,6 +130,7 @@ export interface RelationsQueryFilter {
filters?: Array<RelationEntityTypeFilter>;
maxLevel?: number;
fetchLastLevelOnly?: boolean;
negate?: boolean;
}
export interface EntitySearchQueryFilter {

1
ui-ngx/src/app/shared/models/relation.models.ts

@ -55,6 +55,7 @@ export const directionTypeTranslations = new Map<EntitySearchDirection, string>(
export interface RelationEntityTypeFilter {
relationType: string;
entityTypes: Array<EntityType>;
negate?: boolean;
}
export interface RelationsSearchParameters {

4
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -4030,7 +4030,9 @@
"relation-filters": "Relation filters",
"additional-info": "Additional info (JSON)",
"invalid-additional-info": "Unable to parse additional info json.",
"no-relations-text": "No relations found"
"no-relations-text": "No relations found",
"negate-all-relations": "Negate all relations (NOT condition)",
"negate-relation": "Negate this relation (NOT condition)"
},
"resource": {
"add": "Add resource",

Loading…
Cancel
Save