rsql-jpa-specification icon indicating copy to clipboard operation
rsql-jpa-specification copied to clipboard

Filter nested Collection not work as expected.

Open MichaelKoch11 opened this issue 2 years ago • 3 comments

When filter an Entity with nested @onetomany Collection, like

{
   "_embedded":{
      "tools":[
         {
            "createdAt":"12.12",
            "parts":[
               {
                  "broken":true
               },
               {
                  "broken":false
               }
            ]
         },
         {
            "createdAt":"12.13",
            "parts":[
               {
                  "broken":true
               },
               {
                  "broken":true
               }
            ]
         }
      ]
   }
}
```
I got not expected output when searching  parts.broken==false

````{json}
{
   "_embedded":{
      "tools":[
         {
            "createdAt":"12.12",
            "parts":[
               {
                  "broken":true
               },
               {
                  "broken":false
               }
            ]
         }
            ]
         }
      ]
   }
}
```
Expected is
````{json}
{
   "_embedded":{
      "tools":[
         {
            "createdAt":"12.12",
            "parts":[
               {
                  "broken":false
               }
            ]
         }
      ]
   }
}
```

How can I solve it.

MichaelKoch11 avatar Jun 02 '22 15:06 MichaelKoch11

You should share your code segment at least, so we can take a look c:

alancruzcgi avatar Jul 26 '22 20:07 alancruzcgi

Here are my code segments for full example. My Classes for example Domain

package com.test.domain;
import com.google.common.base.Objects;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.util.UUID;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractEntity implements Serializable {
	@Id
	@GeneratedValue(generator = "UUID")
	@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
	@Column(name = "id", updatable = false, nullable = false)
	private UUID id;
	
	
	@CreationTimestamp
	@Column(name = "create_date", nullable = false, updatable = false)
	private OffsetDateTime createDate;
	
	@UpdateTimestamp
	@Column(name = "modify_date", nullable = false)
	private OffsetDateTime modifyDate;
	
	@CreatedBy
	private String createdBy;
	
	@LastModifiedBy
	private String lastModifiedBy;
	
	
	private boolean deleted;
	
	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (!(o instanceof AbstractEntity)) return false;
		AbstractEntity that = (AbstractEntity) o;
		return deleted == that.deleted && Objects.equal(id, that.id) && Objects.equal(createDate, that.createDate) && Objects.equal(modifyDate, that.modifyDate) && Objects.equal(createdBy, that.createdBy) && Objects.equal(lastModifiedBy, that.lastModifiedBy);
	}
	
	@Override
	public int hashCode() {
		return Objects.hashCode(id, createDate, modifyDate, createdBy, lastModifiedBy, deleted);
	}
}

package com.test.domain;
import com.google.common.base.Objects;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.envers.Audited;

import javax.persistence.*;
import java.io.Serializable;
import java.util.SortedSet;


@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "PackageItem")
@Table(name = "packageItems")
@Audited(withModifiedFlag = true)
public class PackageItem extends AbstractEntity implements Serializable {
	
	
	private double weightKg;
	private double heightCm;
	private double widthCm;
	private double deepCm;
	
	@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "packageItem")
	//@Where(clause = "deleted = false")
	@OrderBy("key ASC")
	private SortedSet<PackagePackageProperty> packagePackageProperties;
	
	@ManyToOne
	private PackageClass packageClass;
	
	public void addPackagePackageProperties(PackagePackageProperty packagePackageProperty) {
		this.packagePackageProperties.add(packagePackageProperty);
		packagePackageProperty.setPackageItem(this);
	}
	
	
	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (!(o instanceof PackageItem packageItem)) return false;
		if (!super.equals(o)) return false;
		return Double.compare(packageItem.weightKg, weightKg) == 0 && Double.compare(packageItem.heightCm, heightCm) == 0 && Double.compare(packageItem.widthCm, widthCm) == 0 && Double.compare(packageItem.deepCm, deepCm) == 0 && Objects.equal(packagePackageProperties, packageItem.packagePackageProperties) && Objects.equal(packageClass, packageItem.packageClass);
	}
	
	@Override
	public int hashCode() {
		return Objects.hashCode(super.hashCode(), weightKg, heightCm, widthCm, deepCm, packagePackageProperties, packageClass);
	}
}
package com.test.domain;

import com.google.common.base.Objects;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.Type;
import org.hibernate.envers.Audited;
import org.jetbrains.annotations.NotNull;

import javax.persistence.*;
import java.io.Serializable;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "PackagePackageProperty")
@Table(name = "packagePackageProperties")
@Audited(withModifiedFlag = true)
public class PackagePackageProperty extends AbstractEntity implements Serializable, Comparable<PackagePackageProperty> {
	
	@Column(name = "key")
	@Type(type = "text")
	private String key;
	
	@Column(name = "value")
	@Type(type = "text")
	private String value;
	
	
	@Column(name = "type")
	@Type(type = "text")
	private String type;
	
	@ManyToOne(fetch = FetchType.LAZY)
	private PackageItem packageItem;
	
	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (!(o instanceof PackagePackageProperty that)) return false;
		if (!super.equals(o)) return false;
		return Objects.equal(key, that.key) && Objects.equal(value, that.value) && Objects.equal(type, that.type);
	}
	
	@Override
	public int hashCode() {
		return Objects.hashCode(super.hashCode(), key, value, type);
	}
	
	
	@Override
	public int compareTo(@NotNull PackagePackageProperty o) {
		return this.key.compareTo(o.getKey());
	}
}

import com.google.common.base.Objects;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.Type;
import org.hibernate.envers.Audited;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Set;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity(name = "PackageClass")
@Table(name = "packageClasses")
@Audited(withModifiedFlag = true)
public class PackageClass extends AbstractEntity implements Serializable {
	
	
	@Column(name = "className")
	@Type(type = "text")
	private String name;
	
	@OneToMany(mappedBy = "packageClass")
	private Set<PackageItem> packageItems;
	
	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (!(o instanceof PackageClass that)) return false;
		if (!super.equals(o)) return false;
		return Objects.equal(name, that.name) && Objects.equal(packageItems, that.packageItems);
	}
	
	@Override
	public int hashCode() {
		return Objects.hashCode(super.hashCode(), name, packageItems);
	}
}

Repository


import com.test.domain.PackageItem;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;

import java.util.Optional;
import java.util.UUID;


@Repository
public interface PackageItemRepository extends JpaRepository<PackageItem, UUID>, JpaSpecificationExecutor<PackageItem>, QuerydslPredicateExecutor<PackageItem> {
	
	
	Optional<PackageItem> findByIdAndDeleted(UUID id, boolean deleted);
	
	
}

import com.test.domain.PackagePackageProperty;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;

import java.util.Optional;
import java.util.UUID;

@Repository
public interface PackageItemPackagePropertyRepository extends JpaRepository<PackagePackageProperty, UUID>, JpaSpecificationExecutor<PackagePackageProperty>, QuerydslPredicateExecutor<PackagePackageProperty> {
	
	Optional<PackagePackageProperty> findByIdAndDeleted(UUID uuid, boolean deleted);
	
	
}

Service

import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.Optional;

public interface CrudServiceExtend<E, P> {
	
	E create(E entity);
	
	E update(P primaryKey, E entity);
	
	E partialUpdate(P primaryKey, JsonMergePatch patch);
	
	Page<E> read(Pageable pageable, String query);
	
	Optional<E> readOne(P primaryKey);
	
	
	void delete(P primaryKey);
	
	
}

import com.test.domain.PackagePackageProperty;

import java.util.UUID;

public interface PackageItemPackagePropertyService extends CrudServiceNested<PackagePackageProperty, UUID> {
}


import com.test.domain.PackageItem;

import java.util.UUID;


public interface PackageItemService extends CrudServiceExtend<PackageItem, UUID> {


}

Service Reduced on Methods which relevant


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jsonpatch.JsonPatchException;
import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
import com.test.controller.dto.PackageItemDTO;
import com.test.domain.PackageClass;
import com.test.domain.PackageItem;
import com.test.domain.PackagePackageProperty;
import com.test.domain.QPackageItem;
import com.test.exeptions.exeption.*;
import com.test.repository.PackageClassRepository;
import com.test.repository.PackageItemPackagePropertyRepository;
import com.test.repository.PackageItemRepository;
import com.test.service.PackageItemPackagePackagePropertyService;
import com.test.service.PackageItemService;
import com.test.service.mapper.PackageItemMapper;
import com.test.service.mapper.PackagePackageClassMapper;
import com.test.service.mapper.PackagePackagePropertyMapper;
import com.test.utils.QueryRewrite;
import com.test.verification.ValidationGroups;
import io.github.perplexhub.rsql.RSQLQueryDslSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.*;
import java.util.stream.Collectors;

import static com.test.domain.QPackageItem.packageItem;


@Service
public class PackageItemServiceBean implements PackageItemService {
	
	private final int[] finalLongestConditionNameArray = {-1};
	@Inject
	Validator validator;
	@Autowired
	private PackageItemRepository repository;
	@Autowired
	private ObjectMapper objectMapper;
	@Autowired
	private PackageItemMapper mapper;
	@Autowired
	private PackageClassRepository classRepository;
	@Autowired
	private PackagePackageClassMapper classMapper;
	@Autowired
	private PackageItemPackagePropertyRepository propertiesRepository;
	@Autowired
	private PackagePackagePropertyMapper packagePackagePropertyMapper;
	@Autowired
	private PackageItemPackagePackagePropertyService packageItemPackagePackagePropertyService;
	
	
	
	@Override
	public Page<PackageItem> read(Pageable pageable, String query) {
		var spec = RSQLQueryDslSupport.toPredicate(query, packageItem);
		var preSelected = repository.findAll(spec, pageable);
		var parsedQuery = RSQLQueryDslSupport.toComplexMultiValueMap(query);
		
		int longestConditionName = 0;
		for (var parsedQueryItem : parsedQuery.keySet()) {
			var conditions = parsedQuery.get(parsedQueryItem);
			for (var condition : conditions.keySet()) {
				if (condition.replaceAll("=", "").length() > longestConditionName) {
					longestConditionName = condition.replaceAll("=", "").length();
				}
			}
		}
		
		int finalLongestConditionName = longestConditionName;
		
		return preSelected.map(packageItem -> {
			if (packageItem.getPackagePackageProperties() == null || packageItem.getPackagePackageProperties().isEmpty()) {
				return packageItem;
			}
			String replaceQuery = QueryRewrite.queryRewritePackageItemToPackagePackageProperty(QueryRewrite.queryDefaultMatcher(query, finalLongestConditionName));
			packageItem.setPackagePackageProperties((SortedSet<PackagePackageProperty>) packageItemPackagePackagePropertyService.read(packageItem.getId(), replaceQuery));
			return packageItem;
		});
		
	}
	
	@Override
	public Optional<PackageItem> readOne(UUID primaryKey) {
		var spec = RSQLQueryDslSupport.toPredicate(QueryRewrite.queryById(primaryKey), QPackageItem.packageItem);
		CompanyServiceBean.finalLongestConditionNameArrayFunction(finalLongestConditionNameArray);
		return repository.findOne(spec).map(packageItem -> {
			if (packageItem.getPackagePackageProperties() == null || packageItem.getPackagePackageProperties().isEmpty()) {
				return packageItem;
			}
			String replaceQuery = QueryRewrite.queryRewritePackageItemToPackagePackageProperty(QueryRewrite.queryDefaultMatcher("packagePackageProperties.deleted==false", finalLongestConditionNameArray[0]));
			packageItem.setPackagePackageProperties((SortedSet<PackagePackageProperty>) packageItemPackagePackagePropertyService.read(packageItem.getId(), replaceQuery));
			return packageItem;
		});
	}
	
		

}



import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jsonpatch.JsonPatchException;
import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
import com.test.controller.dto.PackagePackagePropertyDTO;
import com.test.domain.PackageItem;
import com.test.domain.PackagePackageProperty;
import com.test.domain.QPackagePackageProperty;
import com.test.exeptions.exeption.NoSuchElementFoundException;
import com.test.exeptions.exeption.NoSuchElementFoundOrDeleted;
import com.test.exeptions.exeption.UnprocessableEntityExeption;
import com.test.repository.PackageItemPackagePropertyRepository;
import com.test.repository.PackageItemRepository;
import com.test.service.PackageItemPackagePackagePropertyService;
import com.test.service.mapper.PackagePackagePropertyMapper;
import com.test.verification.ValidationGroups;
import io.github.perplexhub.rsql.RSQLQueryDslSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;


@Service
public class PackageItemPackagePackagePropertyServiceBean implements PackageItemPackagePackagePropertyService {
	
	@Inject
	Validator validator;
	@Autowired
	private PackageItemPackagePropertyRepository packageItemPackagePropertyRepository;
	@Autowired
	private PackageItemRepository packageItemRepository;
	@Autowired
	private ObjectMapper objectMapper;
	@Autowired
	private PackagePackagePropertyMapper mapper;
	

	
	@Override
	public Collection<PackagePackageProperty> read(UUID packageItemId, String query) {
		if (query.trim().isBlank()) {
			query += "packageItem.id==" + packageItemId;
		} else {
			query = "( " + query + " ) and packageItem.id==" + packageItemId;
		}
		
		var spec = RSQLQueryDslSupport.toPredicate(query, QPackagePackageProperty.packagePackageProperty);
		packageItemRepository.findById(packageItemId).orElseThrow(() -> new NoSuchElementFoundException(PackageItem.class.getSimpleName(), packageItemId));
		return StreamSupport
				.stream(packageItemPackagePropertyRepository.findAll(spec).spliterator(), false)
				.collect(Collectors.toCollection(TreeSet::new));
	}
	
	@Override
	public Optional<PackagePackageProperty> readOne(UUID packageItemId, UUID packagePackagePropertiesId) {
		
		return packageItemRepository.findById(packageItemId).map(
						p ->
								Optional.of(
										p
												.getPackagePackageProperties()
												.stream()
												.filter(packageItemPackageProperty -> packageItemPackageProperty.getId().equals(packagePackagePropertiesId))
												.findAny()
												.orElseThrow(() -> new NoSuchElementFoundException(PackagePackageProperty.class.getSimpleName(), packagePackagePropertiesId))
								))
				.orElseThrow(() -> new NoSuchElementFoundException(PackageItem.class.getSimpleName(), packageItemId));
		
	}
	

	
	
}

query rewrite to solve the problem so far


import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Component
public class QueryRewrite {
	
	public static String queryRewriteAll(String query) {
		return query.replaceAll("==[\\s]*all", "=in=(true,false)");
	}
	
	public static Matcher queryDefaultMatcher(String query, int finalLongestConditionName) {
		return Pattern.compile("(?<!=[a-zA-Z]{0," + finalLongestConditionName + "})[\\.]?[a-zA-Z]+=").matcher(query);
	}
	
	public static String queryRewritePackageItemToPackagePackageProperty(Matcher m) {
		return m.replaceAll((match) -> {
					String replaceFilterQuery = match.group();
					if (replaceFilterQuery.startsWith(".")) {
						return replaceFilterQuery;
					} else {
						return "packageItem." + replaceFilterQuery;
					}
				})
				.replaceAll("packageClass", "packageItem.packageClass")
				.replaceAll("packagePackageProperties.", "");
                // Relevant for extended Data MOdell
				//.replaceAll("order", "packageItem.order");
	}
	
	
	public static String queryById(UUID id) {
		return "id==" + id;
	}
}

Controller

public interface CrudControllerExtend<O, P> {
	
	@PostMapping
	ResponseEntity<O> create(O object);
	
	@PutMapping("/{id}")
	ResponseEntity<O> update(@PathVariable("id") P primaryKey, O object);
	
	
	@JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
	@PatchMapping(path = "/{id}", consumes = "application/merge-patch+json")
	ResponseEntity<O> partialUpdate(@PathVariable("id") P primaryKey, @RequestBody JsonMergePatch patch) throws JsonPatchException, JsonProcessingException;
	
	
	@GetMapping
	ResponseEntity<PagedModel<O>> read(@PageableDefault(page = 0, size = Integer.MAX_VALUE) Pageable pageable, @RequestParam(name = "filter", defaultValue = "deleted==false") String query);
	
	
	@GetMapping(path = "/{id}")
	ResponseEntity<O> readOne(@PathVariable("id") P primaryKey);
	
	
	@DeleteMapping("/{id}")
	ResponseEntity delete(@PathVariable("id") P primaryKey);
	
}```
Reduced Controller
```java

import com.fasterxml.jackson.core.JsonProcessingException;
import com.github.fge.jsonpatch.JsonPatchException;
import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
import com.test.assembler.PackageItemAssembler;
import com.test.controller.dto.PackageItemDTO;
import com.test.domain.PackageItem;
import com.test.service.PackageItemService;
import com.test.service.mapper.PackageItemMapper;
import com.test.utils.QueryRewrite;
import com.test.verification.ValidationGroups;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PagedResourcesAssembler;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.PagedModel;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.Optional;
import java.util.UUID;


@RestController
@RequestMapping("/packageitems")
public class PackageItemController implements CrudControllerExtend<PackageItemDTO, UUID> {
	
	@Autowired
	private PackageItemService service;
	@Autowired
	private PackageItemMapper mapper;
	@Autowired
	private PackageItemAssembler packageItemAssembler;
	@Autowired
	private PagedResourcesAssembler<PackageItem> pagedResourcesAssembler;
	
	@Override
	public ResponseEntity<PagedModel<PackageItemDTO>> read(Pageable pageable,
	                                                       @RequestParam(name = "filter", defaultValue = "deleted==false;packagePackageProperties.deleted==false") String query) {
		
		Page<PackageItem> page = service.read(pageable, QueryRewrite.queryRewriteAll(query));
		PagedModel<PackageItemDTO> pages;
		//if (page.hasContent()) {
		pages = pagedResourcesAssembler.toModel(page, packageItemAssembler);
		//} else {
		//	pages = (PagedModel<PackageItemDTO>) pagedResourcesAssembler.toEmptyModel(page, PackageItemDTO.class);
		//}
		return new ResponseEntity<>(pages, HttpStatus.OK);
		
	}
	
	@Override
	public ResponseEntity<PackageItemDTO> readOne(@PathVariable("id") UUID primaryKey) {
		Optional<PackageItem> packageItem = service.readOne(primaryKey);
		if (packageItem.isPresent()) {
			var packageItemDTO = packageItemAssembler.toModel(packageItem.get());
			return new ResponseEntity<>(packageItemDTO, HttpStatus.OK);
		} else {
			
			return new ResponseEntity<>(HttpStatus.NOT_FOUND);
		}
		
	}
	
	
	
	
}

Assembler


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.assembler.wrapper.PackagePackageClassAssemblerWrapper;
import com.test.assembler.wrapper.PackagePackagePropertyAssemblerWrapper;
import com.test.controller.PackageItemController;
import com.test.controller.dto.PackageItemDTO;
import com.test.controller.dto.PackagePackageClassDTO;
import com.test.controller.dto.PackagePackagePropertyDTO;
import com.test.domain.PackageClass;
import com.test.domain.PackageItem;
import com.test.domain.PackagePackageProperty;
import com.test.exeptions.exeption.BadRequestException;
import com.test.service.mapper.PackageItemMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;
import org.springframework.stereotype.Component;

import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

@Component
public class PackageItemAssembler extends RepresentationModelAssemblerSupport<PackageItem, PackageItemDTO> {
	@Autowired
	ObjectMapper objectMapper;
	@Autowired
	private PackageItemMapper packageItemMapper;
	@Autowired
	private PackagePackageClassAssemblerWrapper packageItemPackageClassAssembler;
	@Autowired
	private PackagePackagePropertyAssemblerWrapper packageItemPackagePropertyAssembler;
	
	
	public PackageItemAssembler() {
		super(PackageItemController.class, PackageItemDTO.class);
	}
	
	@Override
	public PackageItemDTO toModel(PackageItem packageItem) {
		
		PackageItemDTO packageItemDTO = packageItemMapper
				.toDto(packageItem)
				.add(linkTo(methodOn(PackageItemController.class).readOne(packageItem.getId())).withSelfRel());
		packageItemDTO.setPackageClass(toClassDTO(packageItem.getId(), packageItem.getPackageClass()));
		packageItemDTO.setPackagePackageProperties(toPropertiesDTO(packageItem.getId(), packageItem.getPackagePackageProperties()));
		return packageItemDTO;
	}
	
	@Override
	public CollectionModel<PackageItemDTO> toCollectionModel(Iterable<? extends PackageItem> entities) {
		return super.toCollectionModel(entities);
		
	}
	
	private PackagePackageClassDTO toClassDTO(UUID packageItemId, PackageClass packageClass) {
		if (packageClass == null) {
			return null;
		} else {
			try {
				var dto = packageItemPackageClassAssembler.toModel(packageClass, packageItemId);
				return objectMapper.treeToValue(objectMapper.valueToTree(dto), PackagePackageClassDTO.class);
			} catch (JsonProcessingException ex) {
				throw new BadRequestException("Cannot map PackageClass Object in " + getClass().getSimpleName());
			}
		}
	}
	
	private SortedSet<PackagePackagePropertyDTO> toPropertiesDTO(UUID packageItemId, SortedSet<PackagePackageProperty> packagePackageProperties) {
		if (packagePackageProperties == null || packagePackageProperties.isEmpty()) {
			return new TreeSet<>();
		} else {
			return new TreeSet<>(packageItemPackagePropertyAssembler.toCollectionModel(packagePackageProperties, packageItemId, false).getContent());
		}
	}
	
}


import com.test.controller.PackageItemPackagePackagePropertyController;
import com.test.controller.dto.PackagePackagePropertyDTO;
import com.test.domain.PackagePackageProperty;
import com.test.service.mapper.PackagePackagePropertyMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport;
import org.springframework.stereotype.Component;

@Component
public class PackagePackagePropertyAssembler extends RepresentationModelAssemblerSupport<PackagePackageProperty, PackagePackagePropertyDTO> {
	@Autowired
	private PackagePackagePropertyMapper packagePackagePropertyMapper;
	
	
	public PackagePackagePropertyAssembler() {
		super(PackageItemPackagePackagePropertyController.class, PackagePackagePropertyDTO.class);
	}
	
	@Override
	public PackagePackagePropertyDTO toModel(PackagePackageProperty entity) {
		return packagePackagePropertyMapper.toDto(entity);
	}
	
	@Override
	public CollectionModel<PackagePackagePropertyDTO> toCollectionModel(Iterable<? extends PackagePackageProperty> entities) {
		return super.toCollectionModel(entities);
	}
	
	
}

import com.test.assembler.PackagePackagePropertyAssembler;
import com.test.controller.PackageItemController;
import com.test.controller.PackageItemPackagePackagePropertyController;
import com.test.controller.dto.PackagePackagePropertyDTO;
import com.test.domain.PackagePackageProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.CollectionModel;
import org.springframework.stereotype.Component;

import java.util.UUID;
import java.util.stream.Collectors;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

@Component
public class PackagePackagePropertyAssemblerWrapper {
	
	@Autowired
	PackagePackagePropertyAssembler packagePackagePropertyAssembler;
	
	
	public PackagePackagePropertyDTO toModel(PackagePackageProperty entity, UUID packageItemId, boolean backwardLink) {
		
		var dto = packagePackagePropertyAssembler.toModel(entity);
		return addLinks(dto, packageItemId, backwardLink);
	}
	
	public CollectionModel<PackagePackagePropertyDTO> toCollectionModel(Iterable<? extends PackagePackageProperty> entities, UUID packageItemId, boolean backwardLink) {
		return CollectionModel.of(packagePackagePropertyAssembler
				.toCollectionModel(entities).getContent()
				.stream()
				.map(packagePropertyDTO -> addLinks(packagePropertyDTO, packageItemId, backwardLink)).collect(Collectors.toList()));
	}
	
	public PackagePackagePropertyDTO addLinks(PackagePackagePropertyDTO dto, UUID packageItemId, boolean backwardLink) {
		var dtoLink = dto.add(linkTo(methodOn(PackageItemPackagePackagePropertyController.class).readOne(packageItemId, dto.getId())).withSelfRel());
		if (backwardLink) {
			dtoLink.add(linkTo(methodOn(PackageItemController.class).readOne(packageItemId)).withRel("packageItem"));
		}
		return dtoLink;
	}
}

Full Data

{
    "_embedded": {
        "packageItems": [
            {
                "id": "217957bf-01f0-4770-be45-efdbf3d1fd71",
                "createDate": "2022-07-27T12:43:47.987147+02:00",
                "modifyDate": "2022-07-27T12:43:47.987147+02:00",
                "deleted": false,
                "packageClass": {
                    "id": "10ea06e2-7944-4cec-b25c-46cde5ad982d",
                    "createDate": "2022-07-27T12:43:47.868156+02:00",
                    "modifyDate": "2022-07-27T12:43:47.868156+02:00",
                    "deleted": false,
                    "name": "palette",
                    "_links": {
                        "self": {
                            "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packageclasses/10ea06e2-7944-4cec-b25c-46cde5ad982d"
                        }
                    }
                },
                "weightKg": 0.0,
                "heightCm": 12.0,
                "widthCm": 0.0,
                "deepCm": 0.0,
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71"
                    }
                },
                "packagePackageProperties": [
                    {
                        "id": "9b33198a-b018-4282-be8e-aa9267abb99b",
                        "createDate": "2022-07-27T12:43:47.996147+02:00",
                        "modifyDate": "2022-07-27T12:43:47.996147+02:00",
                        "deleted": true,
                        "key": "density",
                        "value": " 2500 kg/m3",
                        "type": "string",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packagepackageproperties/9b33198a-b018-4282-be8e-aa9267abb99b"
                            }
                        }
                    },
                    {
                        "id": "d601fcf2-64da-4718-a1a8-57ef137526bc",
                        "createDate": "2022-07-27T12:43:47.999147+02:00",
                        "modifyDate": "2022-07-27T12:43:47.999147+02:00",
                        "deleted": false,
                        "key": "Compression_strength",
                        "value": "800",
                        "type": "int",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packagepackageproperties/d601fcf2-64da-4718-a1a8-57ef137526bc"
                            }
                        }
                    },
                    {
                        "id": "f52201ac-0596-4e3d-a83f-0a21685100e2",
                        "createDate": "2022-07-27T12:43:48.001152+02:00",
                        "modifyDate": "2022-07-27T12:43:48.001152+02:00",
                        "deleted": false,
                        "key": "fragile",
                        "value": "true",
                        "type": "boolean",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packagepackageproperties/f52201ac-0596-4e3d-a83f-0a21685100e2"
                            }
                        }
                    }
                ]
            },
            {
                "id": "e68a1ebc-43c1-4ad4-ae76-e2b086e28581",
                "createDate": "2022-07-27T12:43:48.034148+02:00",
                "modifyDate": "2022-07-27T12:43:48.034148+02:00",
                "deleted": true,
                "packageClass": {
                    "id": "c047e239-5014-4edb-bd9d-e508d72172f5",
                    "createDate": "2022-07-27T12:43:47.878158+02:00",
                    "modifyDate": "2022-07-27T12:43:47.878158+02:00",
                    "deleted": false,
                    "name": "cardboard",
                    "_links": {
                        "self": {
                            "href": "http://localhost:8080/api/v1/packageitems/e68a1ebc-43c1-4ad4-ae76-e2b086e28581/packageclasses/c047e239-5014-4edb-bd9d-e508d72172f5"
                        }
                    }
                },
                "weightKg": 0.0,
                "heightCm": 14.0,
                "widthCm": 0.0,
                "deepCm": 0.0,
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/api/v1/packageitems/e68a1ebc-43c1-4ad4-ae76-e2b086e28581"
                    }
                },
                "packagePackageProperties": [
                    {
                        "id": "22693112-1f58-4261-8ab8-e0ba70682586",
                        "createDate": "2022-07-27T12:43:48.038154+02:00",
                        "modifyDate": "2022-07-27T12:43:48.038154+02:00",
                        "deleted": false,
                        "key": "wood_moisture",
                        "value": "20%",
                        "type": "string",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/api/v1/packageitems/e68a1ebc-43c1-4ad4-ae76-e2b086e28581/packagepackageproperties/22693112-1f58-4261-8ab8-e0ba70682586"
                            }
                        }
                    }
                ]
            }
        ]
    },
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/v1/packageitems?filter=deleted%3D%3Dfalse;packagePackageProperties.deleted%3D%3Dfalse&page=0&size=2000"
        }
    },
    "page": {
        "size": 2000,
        "totalElements": 2,
        "totalPages": 1,
        "number": 0
    }
}

Query: http://localhost:8080/api/v1/packageitems?filter=deleted==false;packagePackageProperties.deleted==false

expected (only works with query rewrite)

{
    "_embedded": {
        "packageItems": [
            {
                "id": "217957bf-01f0-4770-be45-efdbf3d1fd71",
                "createDate": "2022-07-27T12:43:47.987147+02:00",
                "modifyDate": "2022-07-27T12:43:47.987147+02:00",
                "deleted": false,
                "packageClass": {
                    "id": "10ea06e2-7944-4cec-b25c-46cde5ad982d",
                    "createDate": "2022-07-27T12:43:47.868156+02:00",
                    "modifyDate": "2022-07-27T12:43:47.868156+02:00",
                    "deleted": false,
                    "name": "palette",
                    "_links": {
                        "self": {
                            "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packageclasses/10ea06e2-7944-4cec-b25c-46cde5ad982d"
                        }
                    }
                },
                "weightKg": 0.0,
                "heightCm": 12.0,
                "widthCm": 0.0,
                "deepCm": 0.0,
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71"
                    }
                },
                "packagePackageProperties": [
                    {
                        "id": "d601fcf2-64da-4718-a1a8-57ef137526bc",
                        "createDate": "2022-07-27T12:43:47.999147+02:00",
                        "modifyDate": "2022-07-27T12:43:47.999147+02:00",
                        "deleted": false,
                        "key": "Compression_strength",
                        "value": "800",
                        "type": "int",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packagepackageproperties/d601fcf2-64da-4718-a1a8-57ef137526bc"
                            }
                        }
                    },
                    {
                        "id": "f52201ac-0596-4e3d-a83f-0a21685100e2",
                        "createDate": "2022-07-27T12:43:48.001152+02:00",
                        "modifyDate": "2022-07-27T12:43:48.001152+02:00",
                        "deleted": false,
                        "key": "fragile",
                        "value": "true",
                        "type": "boolean",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packagepackageproperties/f52201ac-0596-4e3d-a83f-0a21685100e2"
                            }
                        }
                    }
                ]
            }
                
            
        ]
    },
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/v1/packageitems?filter=deleted%3D%3Dfalse;packagePackageProperties.deleted%3D%3Dfalse&page=0&size=2000"
        }
    },
    "page": {
        "size": 2000,
        "totalElements": 1,
        "totalPages": 1,
        "number": 0
    }
}

Output with Library

{
    "_embedded": {
        "packageItems": [
            {
                "id": "217957bf-01f0-4770-be45-efdbf3d1fd71",
                "createDate": "2022-07-27T12:43:47.987147+02:00",
                "modifyDate": "2022-07-27T12:43:47.987147+02:00",
                "deleted": false,
                "packageClass": {
                    "id": "10ea06e2-7944-4cec-b25c-46cde5ad982d",
                    "createDate": "2022-07-27T12:43:47.868156+02:00",
                    "modifyDate": "2022-07-27T12:43:47.868156+02:00",
                    "deleted": false,
                    "name": "palette",
                    "_links": {
                        "self": {
                            "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packageclasses/10ea06e2-7944-4cec-b25c-46cde5ad982d"
                        }
                    }
                },
                "weightKg": 0.0,
                "heightCm": 12.0,
                "widthCm": 0.0,
                "deepCm": 0.0,
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71"
                    }
                },
                "packagePackageProperties": [
                    {
                        "id": "9b33198a-b018-4282-be8e-aa9267abb99b",
                        "createDate": "2022-07-27T12:43:47.996147+02:00",
                        "modifyDate": "2022-07-27T12:43:47.996147+02:00",
                        "deleted": true,
                        "key": "density",
                        "value": " 2500 kg/m3",
                        "type": "string",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packagepackageproperties/9b33198a-b018-4282-be8e-aa9267abb99b"
                            }
                        }
                    },
                    {
                        "id": "d601fcf2-64da-4718-a1a8-57ef137526bc",
                        "createDate": "2022-07-27T12:43:47.999147+02:00",
                        "modifyDate": "2022-07-27T12:43:47.999147+02:00",
                        "deleted": false,
                        "key": "Compression_strength",
                        "value": "800",
                        "type": "int",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packagepackageproperties/d601fcf2-64da-4718-a1a8-57ef137526bc"
                            }
                        }
                    },
                    {
                        "id": "f52201ac-0596-4e3d-a83f-0a21685100e2",
                        "createDate": "2022-07-27T12:43:48.001152+02:00",
                        "modifyDate": "2022-07-27T12:43:48.001152+02:00",
                        "deleted": false,
                        "key": "fragile",
                        "value": "true",
                        "type": "boolean",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/api/v1/packageitems/217957bf-01f0-4770-be45-efdbf3d1fd71/packagepackageproperties/f52201ac-0596-4e3d-a83f-0a21685100e2"
                            }
                        }
                    }
                ]
            }
        ]
    },
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/v1/packageitems?filter=deleted%3D%3Dfalse;packagePackageProperties.deleted%3D%3Dfalse&page=0&size=2000"
        }
    },
    "page": {
        "size": 2000,
        "totalElements": 1,
        "totalPages": 1,
        "number": 0
    }
}

build.gradle

import org.springframework.boot.gradle.tasks.run.BootRun

plugins {
    id 'org.springframework.boot' version '2.6.6'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id "gae.piaz.layer3gen" version "1.7"
    id "io.freefair.lombok" version "6.4.3"
    id 'application'

}

jar {
    enabled = false
}


group = 'com.test'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
def querydslVersion = '5.0.0'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    localH2Implementation.extendsFrom implementation
    localH2RuntimeOnly.extendsFrom runtimeOnly
}

repositories {
    mavenCentral()
    gradlePluginPortal()
    maven {
        url "https://plugins.gradle.org/m2/"
    }
}


sourceSets {
    localH2 {
        compileClasspath += sourceSets.main.output
        runtimeClasspath += sourceSets.main.output
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-parent:2.7.0'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-hateoas'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-web-services'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.projectlombok:lombok:1.18.24'
    implementation 'org.jetbrains:annotations:23.0.0'

    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'org.postgresql:postgresql:42.4.0'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    implementation 'org.mapstruct:mapstruct:1.5.1.Final'
    implementation 'org.mapstruct:mapstruct-processor:1.5.1.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.1.Final'
    implementation 'org.hibernate:hibernate-spatial'
    implementation 'org.hibernate:hibernate-envers'


    localH2RuntimeOnly 'org.springframework.boot:spring-boot-devtools'
    localH2RuntimeOnly 'com.h2database:h2'
    testImplementation 'com.h2database:h2'
    localH2RuntimeOnly 'org.springdoc:springdoc-openapi-ui:1.6.9'

    implementation 'com.google.guava:guava:31.1-jre'

    implementation 'com.github.java-json-tools:json-patch:1.13'
    implementation 'org.apache.commons:commons-lang3:3.12.0'
    implementation 'io.quarkus:quarkus-hibernate-validator:2.9.2.Final'

    testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
    testImplementation 'org.mockito:mockito-junit-jupiter:4.6.1'
    implementation 'com.jayway.jsonpath:json-path:2.7.0'
    implementation 'org.apache.httpcomponents:httpclient:4.5.13'

    implementation 'io.github.perplexhub:rsql-querydsl-spring-boot-starter:5.0.19'
    implementation group: 'com.querydsl', name: 'querydsl-jpa', version: querydslVersion
    implementation group: 'com.querydsl', name: 'querydsl-apt', version: querydslVersion
    implementation group: 'com.querydsl', name: 'querydsl-core', version: querydslVersion

    testImplementation 'io.github.perplexhub:rsql-querydsl-spring-boot-starter:5.0.19'
    testImplementation group: 'com.querydsl', name: 'querydsl-jpa', version: querydslVersion
    testImplementation group: 'com.querydsl', name: 'querydsl-apt', version: querydslVersion
    testImplementation group: 'com.querydsl', name: 'querydsl-core', version: querydslVersion

    annotationProcessor group: 'com.querydsl', name: 'querydsl-apt', version: querydslVersion
    annotationProcessor group: 'com.querydsl', 'name': 'querydsl-apt', version: querydslVersion, classifier: 'jpa'
    annotationProcessor group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2'
    annotationProcessor group: 'org.hibernate.javax.persistence', name: 'hibernate-jpa-2.1-api', version: '1.0.2.Final'

    implementation 'org.keycloak:keycloak-admin-client:18.0.2'
    testImplementation 'org.keycloak:keycloak-admin-client:18.0.2'
    localH2RuntimeOnly 'org.keycloak:keycloak-admin-client:18.0.2'


}


configurations.implementation {
    exclude group: 'org.jboss.slf4j', module: 'slf4j-jboss-logmanager'

}


configurations.localH2Implementation {
    exclude group: 'org.postgresql', module: 'postgresql'
}

task localH2(type: BootRun) {

    mainClass = "com.test.BackendApplication"
    classpath = sourceSets.localH2.runtimeClasspath
    jvmArgs = ['-Dspring.profiles.active=localH2', '-Dspring.config.additional-location=classpath:application-local.properties,classpath:application-local-security.properties']
}


tasks.named('test') {
    useJUnitPlatform()
    systemProperty "spring.profiles.active", "localH2"
    jvmArgs = ['-Dspring.profiles.active=localH2', '-Dspring.config.additional-location=classpath:application-local.properties,classpath:application-local-security.properties']
}

This problem also exists when only one packet is retrieved. So that the packet properties are not filtered, without query rewrite. If anything else is needed, please let me know.

MichaelKoch11 avatar Jul 27 '22 11:07 MichaelKoch11

I think you need to filter out unwanted child entities which were loaded by JPA. The filter helps to locate the parent entities. Once the targets found, JPA will help to load the associated entities.

One solution is to create a DB view which joining PackageItem and PackagePackageProperty, then create a model class based on the view to avoid filter on nested entities.

perplexhub avatar Aug 03 '22 16:08 perplexhub

Thanks I testing it.

MichaelKoch11 avatar Sep 28 '22 12:09 MichaelKoch11