rsql-jpa-specification
rsql-jpa-specification copied to clipboard
Filter nested Collection not work as expected.
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.
You should share your code segment at least, so we can take a look c:
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.
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.
Thanks I testing it.