how to use spring boot to handle different search? - java

What's the best practice to create search function in spring boot with spring data jpa?
#GetMapping("/search")
public List<Hotel> getAllByCriteria(#RequestParam MultiValueMap<String, String> criteria) {
if (criteria.containsKey("cityPublicId")) {
String cityPublicId = criteria.getFirst("cityPublicId");
if (criteria.containsKey("amenity")) {
List<String> amenities = criteria.get("amenity");
return svc.findAllByCityAndAmenities(cityPublicId, amenities);
}
return svc.findAllByCity(cityPublicId);
}
//currently only support one amenity filtration
else if (criteria.containsKey("amenity")) {
return svc.findAllByAmenities(criteria.get("amenity"));
}
return null;
}
Currently I have to identify all possible combination of criteria to use corresponding method, Is there a universal way to handle all condition? Or at least not hardcoding all possible combination.
PS: If I want to filter result by multiple amenities, may I use findByAmenitiesContains(set)? Where a Hotel entity has a set of amenity. Do I have to create custom query using #query?
Thanks.

You basically have the following options:
create the query programmatically from the input data using a custom method. This gives you maximum flexibility but also requires the most work.
Use a specification. Almost the same flexibility and almost as much work.
Use query by example. Very little work, limited flexibility.
Regarding the PS: The capabilities of query derivation are well documented.

AFAIR you can use different request payload entities to handle the same endpoint
#GetMapping(path = "/search", params = { "cityId" })
public List<Hotel> getAllByCriteria(ByCityPublicId byCity) {
return svc.findAllByCity(byCity.getCityPublicId())
}
#GetMapping(path = "/search", params = { "cityId", "amenity" })
public List<Hotel> getAllByCriteria(ByCityPublicIdAndAmenity byCityAndAmenitities) {
return svc.findAllByCityAndAmenities(byCityAndAmenitities.getCityPublicId(), byCityAndAmenitities.getAmenitities())
}
#GetMapping(path = "/search", params = { "amenity" })
public List<Hotel> getAllByCriteria(ByAmenity byAmenity) {
return svc.findAllByAmenities(byAmenity.getAmenity());
}

Related

Java: How to add mapper for streamed items

I currently have the following controller method (it's a bit simplified to only show the relevant part):
#PostMapping(...)
...
public ResponseEntity<List<PresignedUrlsResponse>> getPresignedUrlBatch(#Valid #RequestBody PresignedUrlsRequest urlsRequest) {
List<PresignedUrlsResponse> presignedUrlResponses = urlsRequest.getRequests().stream().map(request -> {
// TODO: put this in it's own mapping
String url = this.mediaService.getPresignedUrl(request.getObjectId(), request.getBucket());
PresignedUrlsResponse response = new PresignedUrlsResponse();
response.setId(request.getId());
response.setUrl(url);
return response;
}).collect(Collectors.toList());
return ResponseEntity.ok().body(presignedUrlResponses);
}
As mentioned in the TODO, I want to simplify this controller method and add a mapper. I'm only used to mapping requests from a db call for example (in which I will get a List of entities) but not when the service method has to be called for a list of items.
Is there a best practice for this?
MapStruct supports mapping Stream to Collection and Stream to Stream.
However, in your use case you start with List and not with Stream.
You can move your entire logic in a mapper.
e.g.
#Mapper(componentModel = "spring", uses = {
PresignedUrlMappingService.class
})
public interface PresignedUrlsMapper {
List<PresignedUrlsResponse> map(List<PresignedUrlsRequest> requests);
#Mapping(target = "url", source = "request", qualifiedByName = "presignedUrl")
PresignedUrlsResponse map(PresignedUrlsRequest request);
}
Your PresignedUrlMappingService can look like:
#Service
public class PresignedUrlMappingService {
protected final MediaService mediaService;
public PresignedUrlMappingService(MediaService mediaService) {
this.mediaService = mediaService;
}
#Named("presignedUrl")
public String presignedUrl(PresignedUrlsRequest request) {
return this.mediaService.getPresignedUrl(request.getObjectId(), request.getBucket())
}
}
and finally your controller method will look like:
#PostMapping(...)
...
public ResponseEntity<List<PresignedUrlsResponse>> getPresignedUrlBatch(#Valid #RequestBody PresignedUrlsRequest urlsRequest) {
return ResponseEntity.ok().body(presignedUrlsMapper.map(urlsRequest.getRequests());
}

Writing search APIs based on different combination of search params in Spring boot application

I have developed following three APIs for my spring-boot application:
#GetMapping(value = "/state-transitions/searchWithFromState")
public ResponseEntity<List<TcStateTransitionDTO>>
searchWithFromState(
#RequestParam(value = "fromStateId") String fromStateId) {
return ResponseEntity.ok(stateTransitionService.findByFromState(fromStateId));
}
#GetMapping(value = "/state-transitions/searchWithFromStateAndToState")
public ResponseEntity<List<TcStateTransitionDTO>>
searchWithFromStateAndToState(
#RequestParam(value = "fromStateId") String fromStateId,
#RequestParam(value = "toStateId") String toStateId) {
return ResponseEntity.ok(stateTransitionService
.findByFromStateAndToState(fromStateId, toStateId));
}
#GetMapping(value = "/state-transitions/searchWithFromStateAndAction")
public ResponseEntity<List<TcStateTransitionDTO>>
searchWithFromStateAndAction(
#RequestParam(value = "fromStateId") String fromStateId,
#RequestParam(value = "actionId") String actionId) {
return ResponseEntity.ok(stateTransitionService
.findByFromStateAndAction(fromStateId, actionId));
}
These APIs are working perfectly. But I am wondering if is there any way to write these APIs in a better fashion. I am thinking this because, if say, there are n params to search, in this way, I will end up in write 2^n-1 number of APIs.
Could anyone please help here? Thanks.
You can receive variable number of parameters if you receive them as a Map<String,Object> form like this:
#GetMapping(value = "/search")
public ResponseEntity<Page<TcStateTransitionDTO>> search(#RequestParam Map<String, Object> params) {
return ResponseEntity.ok(stateTransitionService.searchByParams(params));
}
You can create a criteria map and generate dynamic query based on the parameters using criteriaBuilder. If you're using JPA, then just pass the specification generated through criteriabuilder to the findAll method.
public Page<TcStateTransitionDTO> searchByParams(Map<String, Object> params) {
PageRequest pageRequest = generatePageRequestFromParams(params);
Specifications specifications = getSearchSpecifications(params);
return repository.findAll(specifications, pageRequest);
}
This getSpecification(Map<String, Object> params) method that I've mentioned is the main gameplayer here. The main trick is to write this method efficiently. I would suggest to read the above link to know more about CriteriaBuilder and do a bit study on specifications.

How can I override a Spring Data REST method without disabling default implementations

I finally found a way to override methods of Spring Data REST with a custom implementation. Unfortunately this disables the default handling.
My Repository should contain findAll and findById exposed over the GET: /games and GET: /games/{id} respectively and save should not be exported because it is overriden by the controller.
#RepositoryRestResource(path = "games", exported = true)
public interface GameRepository extends Repository<Game, UUID> {
Collection<Game> findAll();
Game findById(UUID id);
#RestResource(exported = false)
Game save(Game game);
}
My controller should handle POST: /games, generate the game on the server and return the saved Game.
#RepositoryRestController
#ExposesResourceFor(Game.class)
#RequestMapping("games")
public class CustomGameController {
private final GameService gameService;
public CustomGameController(GameService gameService) {
this.gameService = gameService;
}
#ResponseBody
#RequestMapping(value = "", method = RequestMethod.POST, produces = "application/hal+json")
public PersistentEntityResource generateNewGame(#RequestBody CreateGameDTO createGameDTO, PersistentEntityResourceAssembler assembler) {
Game game = gameService.generateNewGame(createGameDTO);
return assembler.toFullResource(game);
}
}
However when I try to GET: /games it returns 405: Method Not Allowed but POST: /games works as intended. When I change the value of the generateNewGame mapping to "new" all three requests work. But POST: /games/new is no RESTful URL Layout and I would rather avoid it. I don't understand why I get this behaviour and how I may solve it. Does anybody have a clue?
Use #BasePathAwareControllerannotation above your controller to preserve default spring data rest paths and add new custom path base on your need. Although overwrite default spring data rest path.
#BasePathAwareController
public class CustomGameController {
private final GameService gameService;
public CustomGameController(GameService gameService) {
this.gameService = gameService;
}
#ResponseBody
#RequestMapping(value = "", method = RequestMethod.POST, produces =
"application/hal+json")
public PersistentEntityResource generateNewGame(#RequestBody CreateGameDTO
createGameDTO, PersistentEntityResourceAssembler assembler) {
Game game = gameService.generateNewGame(createGameDTO);
return assembler.toFullResource(game);
}
}
Maybe you can do something we usually do in Linux. Set a fake path and link to it.
POST /games ==> [filter] request.uri.euqal("/games") && request.method==POST
==> Redirect /new/games
What you see also is /games.
Don't use /games/new, it may be conflict with things inner Spring.

How to populate database before running tests with Micronaut

I'm looking for a way to execute some SQL scripts before my test class is executed. With Spring I can easily annotate my test class (or test method) with the #Sql annotation. I haven't found any particular way to do the same with Micronaut.
The only way I found was to manually populate the data programmatically in the test method itself, but, in my experience, there are times when you have to perform multiple inserts to test a single case.
I've came up with the following code to test a REST controller:
Code
#Validated
#Controller("/automaker")
public class AutomakerController {
private AutomakerService automakerService;
public AutomakerController(AutomakerService automakerService) {
this.automakerService = automakerService;
}
#Get("/{id}")
public Automaker getById(Integer id) {
return automakerService.getById(id).orElse(null);
}
#Get("/")
public List<Automaker> getAll() {
return automakerService.getAll();
}
#Post("/")
public HttpResponse<Automaker> save(#Body #Valid AutomakerSaveRequest request) {
var automaker = automakerService.create(request);
return HttpResponse
.created(automaker)
.headers(headers -> headers.location(location(automaker.getId())));
}
#Put("/{id}")
#Transactional
public HttpResponse<Automaker> update(Integer id, #Body #Valid AutomakerSaveRequest request) {
var automaker = automakerService.getById(id).orElse(null);
return Objects.nonNull(automaker)
? HttpResponse
.ok(automakerService.update(automaker, request))
.headers(headers -> headers.location(location(id)))
: HttpResponse
.notFound();
}
}
Test
#Client("/automaker")
public interface AutomakerTestClient {
#Get("/{id}")
Automaker getById(Integer id);
#Post("/")
HttpResponse<Automaker> create(#Body AutomakerSaveRequest request);
#Put("/{id}")
HttpResponse<Automaker> update(Integer id, #Body AutomakerSaveRequest request);
}
#MicronautTest
public class AutomakerControllerTest {
#Inject
#Client("/automaker")
AutomakerTestClient client;
#Test
public void testCreateAutomakerWhenBodyIsValid() {
var request = new AutomakerSaveRequest("Honda", "Japan");
var response = client.create(request);
assertThat(response.code()).isEqualTo(HttpStatus.CREATED.getCode());
var body = response.body();
assertThat(body).isNotNull();
assertThat(body.getId()).isNotNull();
assertThat(body.getName()).isEqualTo("Honda");
assertThat(body.getCountry()).isEqualTo("Japan");
}
#Test
public void testUpdateAutomakerWhenBodyIsValid() {
var responseCreated = client.create(new AutomakerSaveRequest("Chvrolet", "Canada"));
assertThat(responseCreated.code()).isEqualTo(HttpStatus.CREATED.getCode());
var itemCreated = responseCreated.body();
assertThat(itemCreated).isNotNull();
var responseUpdated = client.update(itemCreated.getId(), new AutomakerSaveRequest("Chevrolet", "United States"));
assertThat(responseUpdated.code()).isEqualTo(HttpStatus.OK.getCode());
var itemUpdated = responseUpdated.body();
assertThat(itemUpdated).isNotNull();
assertThat(itemUpdated.getName()).isEqualTo("Chevrolet");
assertThat(itemUpdated.getCountry()).isEqualTo("United States");
}
}
I could use a method annotated with #Before to populate all the data I need but it would really be nice to be able to use *.sql scripts the way it is possible with Spring. Is there a way to provide such *.sql scripts before the tests are executed ?
TL;DR — Use Flyway.
With Flyway, you can set up and maintain a given database schema (extremely) easily. To your case, any migration script you put under ../test/resources/db/migration/ (or any other default location you set) will be only visible to your tests, and can be executed/run automatically (if configured) any time you run your tests.
Another solution would be to use an in-memory database (but I would stay away from that for real applications). For instance, H2 have a way to specify an "initialization" script and another for data seeding (and such).

How is the right way to implement HTTP PATCH on Spring MVC?

I have a requirement to implement an HTTP PATCH method in a Spring MVC application. I followed this tutorial: https://www.baeldung.com/http-put-patch-difference-spring.
This is the piece of code:
#RequestMapping(value = "/heavyresource/{id}", method = RequestMethod.PATCH, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> partialUpdateGeneric(
#RequestBody Map<String, Object> updates,
#PathVariable("id") String id) {
heavyResourceRepository.save(updates, id);
return ResponseEntity.ok("resource updated");
}
The problem is that my repository (JPARepository) does not have a method "save" where I can pass a map and an id.
I tried this implementation on my own:
#PatchMapping("/heavyresource/{id}")
public Beer patchUpdate(#RequestBody HeavyResource heavyResource) {
return heavyResourceRepository.save(heavyResource);
}
But it does not work properly because if I pass only one property (that's the point in PATCH) it let's all the others properties as null and I need to update only the property that was passed. Even thinking in DTOs I was not able to implement.
Thanks!

Categories

Resources