Skip to content

Commit

Permalink
#17 BikesServiceImpl: force fetch join on all non-count queries
Browse files Browse the repository at this point in the history
  • Loading branch information
bthreader committed Dec 30, 2023
1 parent 4d016db commit e9324ca
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Predicate;
import lombok.NonNull;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.lang.Nullable;
import servicecourse.generated.types.BikesFilterInput;
import servicecourse.repo.common.SpecificationUtils;
import servicecourse.repo.common.StringFilterSpecification;
Expand All @@ -13,20 +13,27 @@

public class BikeEntitySpecification {
/**
* @param input the details of the filter to apply to the entities
* @return a specification based on the input, if the input is empty the specification will be
* equivalent to "match all"
* @throws NullPointerException if input is null
* Creates a criteria from a filter if specified, also forces fetch join on model and groupset
* embedded entities for non-count based queries. If no filter is specified, or it's empty,
* simply forces fetch join as previously described, will not apply any criteria to entities.
*
* @param input the details of the filter to apply to the entities, can be null
* @return a specification equivalent to the filter input if non-null and non-empty, "match all"
* otherwise. Forces fetch join for embedded entities regardless.
*/
public static Specification<BikeEntity> from(@NonNull BikesFilterInput input) {
public static Specification<BikeEntity> from(@Nullable final BikesFilterInput input) {
return (root, query, cb) -> {
// Force fetch join on all queries apart from the count query used by JPA for paging
if (query.getResultType() != Long.class) {
root.fetch("model", JoinType.INNER);
root.fetch("groupset", JoinType.INNER);
}

List<Predicate> predicates = new ArrayList<>();
if (input == null) {
return SpecificationUtils.alwaysTruePredicate(cb);
}

final List<Predicate> predicates = new ArrayList<>();

// Bike entity predicate
if (input.getSize() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,14 @@
import servicecourse.repo.*;
import servicecourse.repo.common.LongFilterSpecification;
import servicecourse.repo.common.SortUtils;
import servicecourse.repo.common.SpecificationUtils;
import servicecourse.services.common.CursorName;
import servicecourse.services.common.CursorUtils;
import servicecourse.services.common.exceptions.BikeNotFoundException;
import servicecourse.services.common.exceptions.GroupsetNotFoundException;
import servicecourse.services.common.exceptions.ModelNotFoundException;
import servicecourse.services.models.ModelId;

import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import static servicecourse.repo.common.EntityConstants.MINIMUM_ID_VALUE;

Expand All @@ -33,38 +30,34 @@ public class BikesServiceImpl implements BikesService {
private static final int MAXIMUM_FIRST_VALUE = 100;

@Override
public BikeConnection bikes(BikesFilterInput filter, int first, @Nullable CursorInput after) {
public BikeConnection bikes(final BikesFilterInput filter, final int first,
@Nullable final CursorInput after) {
if (first < 1) {
throw new IllegalArgumentException("First must be greater than zero");
}

Optional<Long> afterId = Optional.ofNullable(after)
final Optional<Long> afterId = Optional.ofNullable(after)
.map(CursorInput::getCursor)
.map(cursor -> CursorUtils.decodeBase64CursorToLong(CursorName.AFTER, cursor));

// If specified, the after cursor is the ID of the last bike seen
// Bikes are always returned to the client with IDs in ascending order
// Therefore, if `after` is present, our query is only interested in IDs after this ID
Optional<Specification<BikeEntity>> afterSpecification = afterId
final Optional<Specification<BikeEntity>> afterSpecification = afterId
.map(id -> LongFilterSpecification.newGreaterThanSpecification(id, BikeEntity_.id));

// Combine the (optional) filter and after specifications
List<Specification<BikeEntity>> specifications = Stream.of(
Optional.ofNullable(filter)
.map(BikeEntitySpecification::from),
afterSpecification
)
.flatMap(Optional::stream)
.toList();
Specification<BikeEntity> specification = specifications.isEmpty() ? SpecificationUtils.matchAll()
: Specification.allOf(specifications);
final Specification<BikeEntity> defaultSpecification = BikeEntitySpecification.from(filter);

final Specification<BikeEntity> specification = afterSpecification
.map(s -> Specification.allOf(defaultSpecification, s))
.orElse(defaultSpecification);

// Run the query
Page<BikeEntity> page = bikeRepository.findAll(specification,
PageRequest.of(0,
Math.min(first,
MAXIMUM_FIRST_VALUE),
SortUtils.sortByIdAsc()));
final Page<BikeEntity> page = bikeRepository.findAll(specification,
PageRequest.of(0,
Math.min(first,
MAXIMUM_FIRST_VALUE),
SortUtils.sortByIdAsc()));

return BikeConnection.newBuilder()
.edges(page.get()
Expand All @@ -83,13 +76,13 @@ public BikeConnection bikes(BikesFilterInput filter, int first, @Nullable Cursor
}

@Override
public Bike createBike(CreateBikeInput input) {
ModelEntity modelEntity = modelRepository.findById(ModelId.deserialize(input.getModelId()))
public Bike createBike(final CreateBikeInput input) {
final ModelEntity modelEntity = modelRepository.findById(ModelId.deserialize(input.getModelId()))
.orElseThrow(() -> new ModelNotFoundException(input.getModelId()));
GroupsetEntity groupsetEntity = groupsetRespository.findById(input.getGroupsetName())
final GroupsetEntity groupsetEntity = groupsetRespository.findById(input.getGroupsetName())
.orElseThrow(() -> new GroupsetNotFoundException(input.getGroupsetName()));

BikeEntity newBike = BikeEntity.builder()
final BikeEntity newBike = BikeEntity.builder()
.model(modelEntity)
.groupset(groupsetEntity)
.size(input.getSize())
Expand All @@ -100,22 +93,23 @@ public Bike createBike(CreateBikeInput input) {
}

@Override
public Bike updateBike(UpdateBikeInput input) {
public Bike updateBike(final UpdateBikeInput input) {
return bikeRepository
.findById(BikeId.deserialize(input.getBikeId()))
.map((entity) -> {
// Pull up the groupset, if it has been specified
Optional<GroupsetEntity> groupsetEntity = Optional.ofNullable(input.getGroupsetName())
final Optional<GroupsetEntity> groupsetEntity = Optional.ofNullable(input.getGroupsetName())
.flatMap(name -> {
Optional<GroupsetEntity> result = groupsetRespository.findById(name);
final Optional<GroupsetEntity> result = groupsetRespository.findById(
name);
if (result.isEmpty()) {
throw new GroupsetNotFoundException(name);
}
return result;
});

// Store a copy of the old version of the bike
Bike oldBike = entity.asBike();
final Bike oldBike = entity.asBike();

// Apply the input
entity.apply(UpdateBikeParams.builder()
Expand All @@ -134,7 +128,7 @@ public Bike updateBike(UpdateBikeInput input) {
}

@Override
public Long deleteBike(String id) {
public Long deleteBike(final String id) {
return bikeRepository.findById(BikeId.deserialize(id))
.map((entity) -> {
bikeRepository.deleteById(entity.getId());
Expand Down

0 comments on commit e9324ca

Please sign in to comment.