In my Spring Boot app, I use Java Stream API and map entity values to DTO as shown below:
public RecipeResponse findById(Long id) {
return recipeRepository.findById(id)
.map(RecipeResponse::new)
.orElseThrow(() -> {
return new NoSuchElementFoundException("Not found");
});
}
But my response has also a list and I map this list in the following DTO:
#Data
public class RecipeResponse {
private Long id;
private String title;
private List<RecipeIngredientResponse> ingredients;
public RecipeResponse(Recipe recipe) {
this.id = recipe.getId();
this.title = recipe.getTitle();
this.ingredients = recipe.getRecipeIngredients().stream().map(RecipeIngredientResponse::new).toList();
}
}
I am not sure if it is a good or a proper idea to map the stream in DTO. I think maybe it would be a more proper way to pass List<RecipeIngredientResponse> from service method to this DTO constructor instead of mapping it in DTO as shown above. What is the most proper way for this scenario?
Update: I followed this approach:
#Data
public class RecipeResponse {
private Long id;
private String title;
private List<RecipeIngredientResponse> ingredients;
public RecipeResponse(Recipe recipe, List<RecipeIngredientResponse> ingredients) {
this.id = recipe.getId();
this.title = recipe.getTitle();
this.ingredients = ingredients;
}
}
public RecipeResponse findById(Long id) {
return recipeRepository.findById(id)
.map(recipe -> new RecipeResponse(
recipe,
recipe.getRecipeIngredients().stream().map(RecipeIngredientResponse::new).toList()))
.orElseThrow(() -> {return new NoSuchElementFoundException("Not found");
});
}
In order to follow SOLID principles and have some clear separation of responsibilities and make the code decoupled and easier to test, the recommended approach is to extract the conversion logic into a Converter.
public interface Converter<IN, OUT> {
OUT convert(IN input);
}
Example implementation for converting Recipe to RecipeResponse:
public class RecipeResponseConverter implements Converter<Recipe, RecipeResponse> {
private final Converter<RecipeIngredient, RecipeIngredientResponse> ingredientConverter;
public RecipeResponseConverter(
Converter<RecipeIngredient, RecipeIngredientResponse> ingredientConverter
) {
this.ingredientConverter = ingredientConverter;
}
#Override
public RecipeResponse convert(Recipe input) {
var response = new RecipeResponse();
response.setId(input.getId());
response.setTitle(input.getTitle());
response.setIngredients(input
.getRecipeIngredients()
.stream()
.map(ingredientConverter::convert)
.toList()
);
return response;
}
}
Dependency Injection can be used to inject the recipe converter into the service class and to inject the ingredient converter into the recipe converter without coupling the implementations.
Please note how easy it is, using this approach, to test the various cases for converting a recipe without knowing the details of how an ingredient should be converted since that ingredient converter can just be mocked.
Related
I'm new to reactive programming. I have to create a API call to search-leave-requests. I have to filter leave request by status(PENDING,APPROVED,REJECTED) and logged user role(HR,AHR,RM) and if role is RM(reporting Manager) I have to query from reporting_manager_id and status. If logged user is HR or AHR only need to query from status.
Controller
#GetMapping(value = "/search-leave-requests")
public Mono<List<ViewLeaveRequestsDto>> searchLeaveRequests(#RequestParam("status") String status,
#RequestParam(value = "page", defaultValue = "1") int page,
#RequestParam(value = "size", defaultValue = "9") int size,
#RequestParam(value = "reporting_manager_id") Optional<Long> reporting_manager_id,
#RequestParam(value = "role") String role) {
long startTime = System.currentTimeMillis();
LOGGER.info("searchLeaveRequestsRequest : status={}", status);
return leaveRequestService.searchLeaveRequests(status, page, size, reporting_manager_id, role).map(
response -> {
LOGGER.info("searchLeaveRequestsResponse : timeTaken={}|response={}",
CommonUtil.getTimeTaken(startTime),
CommonUtil.convertToString(response));
return response;
});
}
Service
public interface LeaveRequestService {
public Mono<List<ViewLeaveRequestsDto>> searchLeaveRequests(String status, int page, int size,
Optional<Long> reporting_manager_id, String role);
ServiceImpl
#Override
public Mono<List<ViewLeaveRequestsDto>> searchLeaveRequests(String status, int page, int size, Optional<Long> reporting_manager_id, String role) {
Flux<LeaveRequest> leaveRequestByStatus = leaveRequestRepository.findByStatus(status)
.switchIfEmpty(
Mono.error(new LmsException(ErrorCode.NO_DATA_FOUND, "No Data matching for given Status")))
// .skip((page - 1) * size)
// .limitRequest(size)
.doOnNext(leaveRequest1 -> {
LOGGER.info("searchLeaveRequestsResponse | {}", leaveRequest1);
});
/* */
// AtomicLong sizeOfFlux = new AtomicLong();
// leaveRequestByStatus.subscribe(object -> sizeOfFlux.incrementAndGet());
List<ViewLeaveRequestsDto> dtoList = new ArrayList<>();
Mono<List<ViewLeaveRequestsDto>> dtoListMono = leaveRequestByStatus.flatMap(entity -> {
// for (int i = 0; i < sizeOfFlux.get(); i++) {
ViewLeaveRequestsDto dto = new ViewLeaveRequestsDto();
dto.setLeave_request_id(entity.getId());
dto.setLeave_request_date(entity.getFromDate());
Mono<LeaveType> leaveType = leaveTypeRepository.findById(entity.getLeaveTypeId());
leaveType.subscribe(s -> dto.setLeave_type(s.getTypeName()));
dto.setFrom_date(entity.getFromDate());
dto.setTo_date(entity.getToDate());
dto.setNumber_of_days(entity.getDaysCount());
dto.setReason(entity.getLeaveReason());
dto.setStatus(entity.getStatus().toString());
dto.setAttachment(entity.getAttachment());
dto.setDay_type(entity.getDateType());
dto.setHalf_day_type(entity.getHalfDayType());
dto.setCovering_employee(entity.getCoveringEmployeeId().toString());
dto.setReporting_manager(entity.getReportingManagerId().toString());
// TODO set profile_image, designation and total_leave_available
dtoList.add(dto);
// }
return Flux.fromIterable(dtoList);
}).collectList();
return dtoListMono;
}
Repository
#Repository
public interface LeaveRequestRepository extends ReactiveCrudRepository<LeaveRequest, Long> {
Flux<LeaveRequest> findByStatus(String status);
Flux<LeaveRequest> findByStatusAndReportingManagerId(String status, Optional<Long> reporting_manager_id);
}
LeaveRequest Entity
#Table(name = "leave_request")
#Data
#AllArgsConstructor
#NoArgsConstructor
public class LeaveRequest {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column("id")
private Long id;
#Column("employee_id")
private Long employeeId;
#Column("date_type")
private Boolean dateType;
#Column("half_day_type")
private Boolean halfDayType;
#Column("leave_request_date")
private LocalDate leaveRequestDate;
#Column("from_date")
private LocalDate fromDate;
#Column("to_date")
private LocalDate toDate;
#Column("leave_reason")
private String leaveReason;
#Column("attachment")
private String attachment;
#Column("days_count")
private Integer daysCount;
#Enumerated(EnumType.STRING)
private Status status;
#Column("reporting_manager_id")
private Long reportingManagerId;
#Column("covering_employee_id")
private Long coveringEmployeeId;
#Column("leave_type_id")
private Long leaveTypeId;
}
ViewLeaveRequestsDto
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
#Data
public class ViewLeaveRequestsDto {
private Long leave_request_id;
private String profile_image;
private String designation;
private String requester_name;
private LocalDate leave_request_date;
private String leave_type;
private Boolean day_type;
private Boolean half_day_type;
private String covering_employee;
private LocalDate from_date;
private LocalDate to_date;
private Integer number_of_days;
private String reason;
private Integer total_leave_available;
private String attachment;
private String reporting_manager;
private String status;
}
When I run this code, I got duplicate records. I have only 3 records in my DB. But in postman response I got many records. Any idea How to fix this ?
PS: My java version - 18
spring-boot version - 2.7
It's worth to take into account that you're mixing the imperative-style code with functional-style code here, which is incorrect way to use reactive.
First: you're calling subscribe() inside the method of your service. Avoid this.
Second: you have created List<ViewLeaveRequestsDto> dtoList = new ArrayList<>(); and you're adding elements in there from flatMap() function which is incorrect. flatMap() is async and concurrent by it's nature, so this approach can lead to problems with thread-safety and etc.
Third: why are you using Mono<List<T>> in your controller if you can return Flux<T> directly?
Having that said I think your code inside your service should look like:
return leaveRequestRepository.findByStatus(status)
.switchIfEmpty(
Mono.error(new LmsException(ErrorCode.NO_DATA_FOUND, "No Data matching for given Status")))
.doOnNext(leaveRequest1 -> {
LOGGER.info("searchLeaveRequestsResponse | {}", leaveRequest1);
})
.flatMap(entity -> leaveTypeRepository.findById(entity.getLeaveTypeId())
.map(leaveType -> {
// build your DTO using leaveType and fields from entity and return DTO
})
);
And change the signature of your method to return Flux of your DTOs
and also change the signature of your controller method to return Flux
UPDATED
Shortly answering why you're getting dupplicates:
Let's say you have 3 rows/documents in your db.
You start streaming them from db with Flux, on each element you call .flatMap() and there you add element (updating your collection every flatMap() call) to your ArrayList. From this flatMap() you return Flux that is made from your ArrayList (updated ArrayList).
This flux "merges" to your "main" flux.
In the resulting list (.collectList())you will get:
1 element + 2 elements + 3 elements = 6 elements in summary
So as I said above, you're handling incorrectly with your initial Flux.
UPDATED_2
I think I figured out why you want to return Mono<List<DTO>> instead of Flux<DTO>, I noticed that in your controller you call .map() to measure the total time taken to collect list of your DTOs.
Two things:
If for some reason you want to return Mono<List<DTO>>, then it's not a problem, in my code (inside your service) just call .collectList() in the end, so you'll return Mono<List<DTO>>, but you should know that this approach forces to collect all the results in memory in time.
.map() operator semantically assumes that you want to map the given object to some another or to change something within that object.
If you want to just log something using this object or do another kind of things (some side effects), then use, for example, doOnNext() operator
I feel like this should be pretty straightforward, but I'm not sure about the actual code for it. Basically, I have my rest controller taking in 6 arguments, passing that through the Service and then using those arguments to build the object inside of the ServiceImplementation. From there I return a call to my repo using the object I just made. This call should attempt to query the database specific parameters of the object.
This query is the part where I'm not sure how to write using Spring JPA standards. I'd like to just use the variables I set my object with, but I'm not sure if I'll have to write out a query or if spring JPA can make it a bit more simple?
Code:
Controller:
#RestController
public class exampleController {
#Autowired
private ExampleService exampleService;
#GetMapping("/rest/example/search")
public exampleObj searchExample (#RequestParam(value = "exLetter") String exLetter,
#RequestParam(value = "exLang") String exLang, #RequestParam(value = "exType")int exType,
#RequestParam(value = "exMethod") String exMethod, #RequestParam(value = "exCd") String exCd,
#RequestParam(value = "exOrg") String exOrg) {
return exampleService.getExampleLetter(exLetter, exLang, exType, exMethod, exCd, exOrg);
}
}
ExampleSerivce:
public interface ExampleService {
public ExampleLetter getExampleLetter(String exLetter, String exLang, int exType, String exMethod, String exCd, String exOrg);
}
ExampleServiceImplementation:
#Service
public class ExampleServiceImpl implements ExampleService {
#Autowired
private ExampleRepository exampleRepo;
#Override
public ExampleLetter getExampleLetter(String exLetter, String exLang, int exType, String exMethod, String exCd, String exOrg) {
ExampleLetter examp = new ExampleLetter();
examp.setExCd(exCd);
examp.getKey().setExampleNumber(exLetter);
examp.getKey().setLanguageType(exLang);
examp.getKey().setMethod(exMethod);
examp.getKey().setMarketOrg(exOrg);
examp.getKey().setType(exType);
return exampleRepo.findExampleLetter(examp);
}
}
Repo:
#Repository
public interface ExampleRepository extends CrudRepository<ExampleLetter, ExampleLetterKey> {
}
If I understand it correctly, you are trying to make a dinamic query, based on filtering values that may or may not be there. If that's the case, you can use the Specification class to create the query dinamically:
First, in your Repository class, extend JpaSpecificationExecutor<ExampleLetter>:
#Repository
public interface ExampleRepository extends CrudRepository<ExampleLetter, ExampleLetterKey>, JpaSpecificationExecutor<ExampleLetter> {
}
Now, you will need a method (I'd sugest you put it in an specific class, for organization sake) to generate the query itself:
public class GenerateQueryForExampleLetter {
ExampleLetter exampleLetter;
public Specification<ExampleLetter> generateQuery() {
return new Specification<ExampleLetter>() {
private static final long serialVersionUID = 1L;
#Override
public Predicate toPredicate(Root<ExampleLetter> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
Predicate pred = null;
List<Predicate> predicates = new ArrayList<Predicate>();
if (this.exampleLetter.getExCd()!= null && !this.exampleLetter.getExCd().isEmpty()) {
predicates.add(builder.equal(root.<String>get("exCd"), this.exampleLetter.getExCd()));
}
...................
if (this.exampleLetter.getTheFieldYouNeed()!= null && !getTheFieldYouNeed.isEmpty()) {
predicates.add(builder.equal(root.<TheTypeOfTheField>get("theFieldYouNeed"), this.exampleLetter.getTheFieldYouNeed()));
}
if (!predicates.isEmpty()) {
pred = builder.and(predicates.toArray(new Predicate[] {}));
}
return pred;
}
};
}
public void setExampleLetter (ExampleLetter el) {
this.exampleLetter = el;
}
}
Finally, in your service class:
#Override
public ExampleLetter getExampleLetter(String exLetter, String exLang, int exType, String exMethod, String exCd, String exOrg) {
ExampleLetter examp = new ExampleLetter();
examp.setExCd(exCd);
examp.getKey().setExampleNumber(exLetter);
examp.getKey().setLanguageType(exLang);
examp.getKey().setMethod(exMethod);
examp.getKey().setMarketOrg(exOrg);
examp.getKey().setType(exType);
GenerateQueryForExampleLetter queryGenerator = new GenerateQueryForExampleLetter ();
queryGenerator.setExampleLetter(examp);
return exampleRepo.findAll(queryGenerator.generateQuery());
}
Note that the JpaSpecificationExecutor interface adds a few utility methods for you to use which, besides filtering, supports sorting and pagination.
For more details, check here, here, or this answer.
I'm creating a Spring boot REST API which should take 2 Lists of custom objects. I'm not able to correctly pass a POST body to the API I've created. Any idea what might be going wrong ?
Below is my code :
Controller Class Method :
// Main controller Class which is called from the REST API. Just the POST method for now.
#RequestMapping(value = "/question1/solution/", method = RequestMethod.POST)
public List<Plan> returnSolution(#RequestBody List<Plan> inputPlans, #RequestBody List<Feature> inputFeatures) {
logger.info("Plans received from user are : " + inputPlans.toString());
return planService.findBestPlan(inputPlans, inputFeatures);
}
Plan Class , this will contain the Feature class objects in an array:
public class Plan {
public Plan(String planName, double planCost, Feature[] features) {
this.planName = planName;
this.planCost = planCost;
this.features = features;
}
public Plan() {
}
private String planName;
private double planCost;
Feature[] features;
public String getPlanName() {
return planName;
}
// getters & setters
}
Feature POJO Class :
// Feature will contain features like - email , archive etc.
public class Feature implements Comparable<Feature> {
public Feature(String featureName) {
this.featureName = featureName;
}
public Feature() {
}
private String featureName;
// Getters / Setters
#Override
public int compareTo(Feature inputFeature) {
return this.featureName.compareTo(inputFeature.getFeatureName());
}
}
You cannot use #RequestBody twice!
You should create a class that holds the two lists and use that class with #RequestBody
You should create json like this:
{
"inputPlans":[],
"inputFeatures":[]
}
and create Class like this:
public class SolutionRequestBody {
private List<Plan> inputPlans;
private List<Feature> inputFeatures;
//setters and getters
}
POST mapping like this:
#RequestMapping(value = "/question1/solution/", method = RequestMethod.POST)
public List<Plan> returnSolution(#RequestBody SolutionRequestBody solution) {
logger.info("Plans received from user are : " + solution.getInputPlans().toString());
return planService.findBestPlan(solution);
}
As we all know, there is a big problem with a partial update of the entity. Since the automatic conversion from json strings to the entity, all fields that have not been transferred will be marked null. And as a result, the fields that we did not want to reset will be reset.
I will show the classical scheme:
#RestController
#RequestMapping(EmployeeController.PATH)
public class EmployeeController {
public final static String PATH = "/employees";
#Autowired
private Service service;
#PatchMapping("/{id}")
public Employee update(#RequestBody Employee employee, #PathVariable Long id) {
return service.update(id, employee);
}
}
#Service
public class Service {
#Autowired
private EmployeeRepository repository;
#Override
public Employee update(Long id, Employee entity) {
Optional<T> optionalEntityFromDB = repository.findById(id);
return optionalEntityFromDB
.map(e -> saveAndReturnSavedEntity(entity, e))
.orElseThrow(RuntimeException::new);
}
private T saveAndReturnSavedEntity(Employee entity, Employee entityFromDB) {
entity.setId(entityFromDB.getId());
return repository.save(entity);
}
}
#Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
and as I have already said that in the current implementation we will not be able to perform a partial update in any way. That is, it is impossible to send an update of only one field in a json line; all fields will be updated, and in null (excepted passed).
The solution to this problem is that you need to perform the conversion from string json to the entity in manual. That is, do not use all the magic from Spring Boot (which is very sad).
I will also give an example of how this can be implemented using merge at the json level:
#RestController
#RequestMapping(EmployeeRawJsonController.PATH)
public class EmployeeRawJsonController {
public final static String PATH = "/raw-json-employees";
#Autowired
private EmployeeRawJsonService service;
#PatchMapping("/{id}")
public Employee update(#RequestBody String json, #PathVariable Long id) {
return service.update(id, json);
}
}
#Service
public class EmployeeRawJsonService {
#Autowired
private EmployeeRepository employeeRepository;
public Employee update(Long id, String json) {
Optional<Employee> optionalEmployee = employeeRepository.findById(id);
return optionalEmployee
.map(e -> getUpdatedFromJson(e, json))
.orElseThrow(RuntimeException::new);
}
private Employee getUpdatedFromJson(Employee employee, String json) {
Long id = employee.getId();
updateFromJson(employee, json);
employee.setId(id);
return employeeRepository.save(employee);
}
private void updateFromJson(Employee employee, String json) {
try {
new ObjectMapper().readerForUpdating(employee).readValue(json);
} catch (IOException e) {
throw new RuntimeException("Cannot update from json", e);
}
}
}
#Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
With this solution, we eliminate the problem associated with the partial update.
But here another problem arises, that we are losing the automatic addition of validation of beans.
That is, in the first case, validation is enough to add one annotation #Valid:
#PatchMapping("/{id}")
public Employee update(#RequestBody #Valid Employee employee, #PathVariable Long id) {
return service.update(id, employee);
}
But we can't do the same when we perform manual deserialization.
My question is, is there any way to enable automatic validation for the second case?
Or maybe there are other solutions that allow you to use Spring Boot magic for Bean Validation.
What you need is not the normal validation , which can achieved through manual validator call.Let’s now go the manual route and set things up programmatically:
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<User>> violations = validator.validate(object);
for (ConstraintViolation<User> violation : violations) {
log.error(violation.getMessage());
}
To validate a bean, we must first have a Validator object, which is constructed using a ValidatorFactory.
Normal validations on Spring Controllers specified with #Valid annotations are triggered automatically during the DataBinding phase when a request is served.All validators registered with the DataBinder will be executed at that stage. We can't do that for your case, so you can manually trigger the validation like above.
I am passing a request body to a POST request on postman similar to this:
"name":"Mars",
"artifacts":[
{
"elements":[
{
"name":"carbon",
"amount":0.5,
"measurement":"g"
}
],
"typeName":"typeA"
},
{
"elements":[
{
"name":"hydrogen",
"amount":0.2,
"measurement":"g"
}
],
"typeName":"typeB"
}
]
The create method in the rest controller looks like this.
#RequestMapping("/create")
public Planet create(#RequestBody Planet data) {
Planet mars = planetService.create(data.getName(),data.getArtifacts());
return mars;
Planet and all its nested objects have a default constructor such as:
public Planet() {}
However, I am not able to create a new planet object because of lack of a default constructor. Please help!
EDIT:
Planet class
public class Planet {
#JsonProperty("name")
private String name;
#Field("artifacts")
private List<Artifact> artifacts;
public Planet() {}
public Planet(String name, List<Artifact> artifacts)
{
this.name = name;
this.artifacts = artifacts;
}
//setters and getters
}
Artifact class:
public class Artifact() {
#Field("elements")
private List<Element> elements;
#JsonProperty("typeName")
private String typeName;
public Artifact() {}
public Artifact(String typeName, List<Element> elements)
{
this.typeName = typeName;
this.elements = elements;
}
}
Element class:
public class Element() {
#JsonProperty("elementName")
private String name;
#JsonProperty("amount")
private double amount;
#JsonProperty("measurement")
private String measurement;
public Element() {}
public Element(String name, double amount, String measurement)
{
//assignments
}
}
I had that the same error when I forgot the #RequestBody before the parameter
#RequestMapping("/create")
public Planet create(#RequestBody Planet data) {
I don't understand what is the issue you are facing, but i can see an error straight away so guessing that is the issue you are facing, i am going to give you a solution.
Create a class which matches your json data structure like this :
Class PlanetData {
private String name;
private List<Planet> artifacts;
public PlanetData(String name, List<Planet> artifacts){
name = name;
artifacts = artifacts;
}
// include rest of getters and setters here.
}
Then your controller should look like this. Basically you needed to put #RequestBody to all the parameters you want to recieve from request JSON. Earlier you only put #RequestBody to name parameter not artifact parameter and since Request Body can be consumed only once, so you need a wrapper class to recieve the complete request body using single #RequestBody annotation.
#RequestMapping("/create")
public String create(#RequestBody PlanetData data) {
Planet mars = planetService.create(data.getName(),data.getArtifacts());
return mars.toString();
}
Edit : Looking at the Planet class, it also needs some modification
public class Planet {
private String typeName; // key in json should match variable name for proper deserialization or you need to use some jackson annotation to map your json key to your variable name.
private List<Element> elements;
public Planet() {}
public Planet(String typeName, List<Element> elements)
{
this.typeName = typeName;
this.elements = elements;
}
//setters and getters. Remember to change your setters and getter from name to typeName.
}
Hope this solves your issue.
This answer too might help someone.
When you are using spring framework for your API development, you may accidently import a wrong library for RequestBody and RequestHeader annotations.
In my case, I accidently imported library,
io.swagger.v3.oas.annotations.parameters.RequestBody
This could arise the above issue.
Please ensure that, you are using the correct library which is
org.springframework.web.bind.annotation.RequestBody
I guess, it’s trying to call new List() which has no constructor. Try using ArrayList in your signatures.
If it works this way, you have found the error. Then rethink your concept of calling methods, since you would usually want to avoid using implementations of List in method signatures
Make sure your request type is not of type GET
If so it is better not to send data as request body.
you should write as below:
...
public String create(#RequestBody JSONObject requestParams) {
String name=requestParams.getString("name");
List<Planet> planetArtifacts=requestParams.getJSONArray("artifacts").toJavaList(Planet.Class);
...