Σάββατο 1 Αυγούστου 2020

Spring JPA Text Search in multiple columns with JpaSpecification






Consider an "Activity"entity with below String fields, to include in our search:





Searching with "GAS", returns 3,4,6,10 IDs:



Database:











1. Controller class



@PostMapping("/search") public ResponseEntity<List<ActivityDTO>> searchAvailableActivities(@Valid @RequestBody SearchDTO searchDTO) { final List<ActivityDTO> workplaceDTOList = searchService.searchAvailableActivities(searchDTO); return new ResponseEntity<>(workplaceDTOList, HttpStatus.OK); }



2. Service class



@Override public Page<ActivityDTO> searchAvailableActivities(SearchDTO searchDTO) { List<ActivityDTO> activityList = this.searchAvailableActivitiesMethod(searchDTO); return activityList; } // Returns activities entities converted to DTOs public List<ActivityDTO> searchAvailableActivitiesMethod(SearchDTO searchDTO) { List<Activity> activityList = searchAvailableActivitiesLogic(searchDTO); return activityList.stream().map(activityMapper::toDto).collect(Collectors.toList()); } // Finds activities from search query's strings public List<Activity> searchAvailableActivitiesLogic(SearchDTO searchDTO) { Set<Activity> activityList = searchActivities(getSearchTerms(searchDTO.getSearchQuery())); return new ArrayList<>(activityList); } // Retrieves search words separated by comma public List<String> getSearchTerms(String searchQuery) { if (searchQuery == null || searchQuery.isEmpty()) { return new ArrayList<>(); } return Arrays.stream(searchQuery.split(",")).map(String::trim).collect(Collectors.toList()); } // Gathers found activities in Set, to avoid duplicates public Set<Activity> searchActivities(List<String> searchTerms) { if (searchTerms == null || searchTerms.isEmpty() || searchTerms.stream().allMatch(String::isEmpty)) { return new HashSet<>(); } return new HashSet<>(searchContainsTerms(searchTerms)); } // Executes searching per query's term, using JpaSpecifications public List<Activity> searchContainsTerms(List<String> searchTerms) { if (searchTerms == null || searchTerms.isEmpty() || searchTerms.stream().allMatch(String::isEmpty)) { return new ArrayList<>(); } List<Activity> result = new ArrayList<Activity>(); for (String term : searchTerms) { Iterable<Activity> activities = this.activityRepository .findAll(CustomJpaSpecifications.hasTitleWithContainingTerm(term) .or(CustomJpaSpecifications.hasSubActivityWithContainingTerm(term)) ); for (Activity act : activities) { result.add(act); } } return result; }


3. Repository class



@Repository public class CustomJpaSpecifications { public static Specification<Activity> hasSubActivityWithContainingTerm(String term) { return (act, cq, cb) -> cb.like(act.get("activitySubType"), "%" + term + "%"); } public static Specification<Activity> hasTitleWithContainingTerm(String term) { return (act, cq, cb) -> cb.like(act.get("title"), "%" + term + "%"); } }