Projectreactor: the parallel execution of queries and concatenating the results - java

Good evening.
I am studying reactive programming and I have encountered the following problem.
I am running two parallel queries to the database and want to combine the results and give them back
#GetMapping
public Mono<User> get(#RequestParam("id") String id, #RequestParam("cId") String cId) {
Mono<User> userMono = Mono.fromCallable(() -> userServ.get(id))
.flatMap(userMono1 -> userMono1)
.subscribeOn(Schedulers.boundedElastic());
Mono<Comment> ger = Mono.fromCallable(() -> commentServ.ger(cId))
.flatMap(commentMono -> commentMono)
.subscribeOn(Schedulers.boundedElastic());
return Mono.zip(userMono, ger)
.map(pair -> {
User t1 = pair.getT1();
t1.setComment(pair.getT2());
return t1;
});
But the point is that the comment may be empty, and then I expect to return the json of such a structure
{
"id": "5e6cbf395214a42f51b57121",
"name": "Bob",
"surname": null,
"comment": null
}
Instead I get an empty response. Apparently this is due to mono zip, but how else can I combine the results, while maintaining query parallelism
My entities:
#Document
#AllArgsConstructor
#NoArgsConstructor
#Data
public class Comment {
#Id
String id;
String userId;
String comment;
}
#Document
#AllArgsConstructor
#NoArgsConstructor
#Data
public class User {
#Id
private String id;
private String name;
private String surname;
private Comment comment;
}
How can I resolve this situation?

Zip/ZipWith need elements to produce their output. If it could be empty , you could use below methods to set some default value.
defaultIfEmpty(new Comment()) or
switchIfEmpty(Mono.fromSupplier(() -> new Comment())
If you do not want to use new Comment() and set null to comment object, we can try this way.
userMono
.zipWith(commentMono.defaultIfEmpty(new Comment()))
.map(pair -> {
User user = pair.getT1();
Comment comment = pair.getT2();
if(Objects.nonNull(comment.getUserId()))
user.setComment(comment);
return user;
});

Related

Convert Java object list to entity list

I have multiple objects in my array using . If I then send this to my Spring Boot backend with axios and output the FormData beforehand, I get the following image. That fits. In the backend, however, I need this list of objects as an entity. In this case, of type List. Do I do that?
Frontend code:
let data = new FormData();
...
data.append("zugeordnet", JSON.stringify(personNamen));
await axios.post("/neuerEintrag", data,...)
React:
Backend:
#PostMapping("/neuerEintrag")
public String neuerEintrag(HttpServletRequest req,#RequestParam("zugeordnet") List<?> zugeordnet,..) {
List<User> userListe = (List<User>) zugeordnet;
for(User inListe : userListe) //ERROR here
{
System.out.println("USER :" + inListe);
}
...
}
java.lang.ClassCastException: class java.lang.String cannot be cast to class com.home.calendar.User.User
UPDATE
For completeness, here is the user entity and the complete method for a new entry.
#PostMapping("/neuerEintrag")
public String neuerEintrag(HttpServletRequest req, #RequestParam("beschreibung") String beschreibung,
#RequestParam("datum") Date datum, #RequestBody List<User> zugeordnet,
#RequestBody List<Freunde> kontaktAuswahl, #RequestParam("neuAlt") String neuAlt,
#RequestParam("kalenderId") int kalenderId) { }
The User Entity:
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
#JsonIgnoreProperties("user")
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "user")
private List<Kalender> kalenderEinträge;
public User() {
super();
// TODO Auto-generated constructor stub
}
public User(String name, List<Kalender> kalenderEinträge) {
super();
this.name = name;
this.kalenderEinträge = kalenderEinträge;
}
public List<Kalender> getKalenderEinträge() {
return kalenderEinträge;
}
[getter/setter]
Spring can't parse an unknown object.
To get it work, I suggest a new class for the "request".
#Data // lombok - this generates getter/setters/equals/hashcode for you
public class NeuerEintragRequest {
private List<User> zugeordnet;
private String beschreibung;
private int kalendarId;
// and your others fields
}
The controller can now use very type-safe objects.
#PostMapping("/neuerEintrag")
public String neuerEintrag(#RequestBody NeuerEintragRequest request) {
for(User user : request.getUserlist()) {
// a logging framework is a lot better. Try to use log4j or slf4j.
log.info("USER: {}", user);
}
...
}
Typescript
Let axios handle the typing and serializing. See this tutorial: https://masteringjs.io/tutorials/axios/post-json
To post all the needed data, you can create a new object.
// no formdata - just send the object
const data = { zugeordnet: personNamen, kalendarId: 123, beschreibung: 'abc' };
await axios.post("/neuerEintrag", data);
You can also create a interface in typescript, but this is going to much for a stackoverflow-answer. Try to learn more about spring and typescript.
Based on question & comments ,
your front end call data.append("zugeordnet", JSON.stringify(personNamen)); is converting your object to List<String> instead of List<User>.
So you can transform this List<String> to List<User> in your postMapping:
#PostMapping("/neuerEintrag")
public String neuerEintrag(HttpServletRequest req,#RequestParam("zugeordnet") List<?> zugeordnet,..) {
ObjectMapper mapper=new ObjectMapper();
for(String str:zugeordnet){
System.out.println(mapper.readValue(str, User.class));
}
...
}

How to loop through a list payload and insert into database as individual row items in Springboot?

I am trying to insert items in a list in a database as single values in rows with the name of the sender. I am able to send the payload and insert into a single row with the user detailst. How can I loop through the payload sent and insert all the items into individual rows? I have tried to look for examples no luck. So far I can only insert as a single row in the database
this is the payload
{"labsigned":["234568","234567","2345678","2344556","12335677","2345677","234556","234545"]}
My controller
#RequestMapping(path = "/labreport/createrordispatched", method = RequestMethod.POST)
public ResponseEntity<?> createDispatched(#RequestBody Dispatched dispatched){
if(labDashboardService.createDispatched(dispatched)) {
return ResponseEntity.status(HttpStatus.CREATED).body(true);
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(false);
}
My Service
public boolean createDispatched(Dispatched dispatched) {
dispatched.setCreatedBy(getCurrentUserEmail());
dispatched.setDateCreated(System.currentTimeMillis());
Dispatched ticket = new Dispatched(
dispatched.getCreatedBy(),
dispatched.getDateCreated(),
dispatched.getlabsigned()
);
dispatchedRepository.save(ticket);
return false;
}
My Model
#Entity
#Table(name = "DISPATCHED")
public class Dispatched {
private String id;
private String labsigned;
private Long dateCreated;
private String createdBy;
public Dispatched(){}
public Dispatched(String createdBy, Long dateCreated, String labsigned){
this.labsigned = rorlabsigned;
this.dateCreated = dateCreated;
this.createdBy = createdBy;
}
Assuming that you were able to insert all labsigned in the payload into one single row with the code you mentioned in the question, You should iterate dispatched.labsigned and insert one by one as rows to accomplish what you need. And returning false at the end of method createDispatched will always return HttpStatus.BAD_REQUEST even though the records are successfully saved in the DB, so you might need to change it to return true.
public boolean createDispatched(Dispatched dispatched) {
List<Dispatched> newTickets = new ArrayList<>();
dispatched.setCreatedBy(getCurrentUserEmail());
dispatched.setDateCreated(System.currentTimeMillis());
for(String labSigned:dispatched.getlabsigned()){
Dispatched ticket = new Dispatched(
dispatched.getCreatedBy(),
dispatched.getDateCreated(),
labSigned
);
newTickets.add(ticket);
}
dispatchedRepository.saveAll(newTickets);
return true;
}
Just send in a list of those values. Shouldn't have to be wrapped in a named field on an object. Just send it in as a json array like ["234568","234567","2345678","2344556","12335677","2345677","234556","234545"]. In your controller method, body don't pass it in as Dispatched but instead a List and then just loop through those creating a list of Dispatch objects and then using saveAll in the repository passing in the newly created Dispatched list.
Update: Example without actually compiling. Should be good enough for the example. Also using lombok to make it easier to read and a few other updates.
#AllArgsConstructor
#FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
#RestController
public class DispatchController {
DispatchedEntityFactory dispatchedEntityFactory;
DispatchedRepository dispatchedRepository;
#PostMapping("/labreport/createrordispatched")
public ResponseEntity<Boolean> createDispatched(DispatchedRequest dispatchedRequests){
List<DispatchedEntity> dispatchedEntities = dispatchedEntityFactory.creatMultipleFromDispatchRequest(dispatchedRequests);
if(CollectionUtils.isEmpty(dispatchedEntities)) {
return ResponseEntity.badRequest().body(false);
}
dispatchedRepository.saveAll(dispatchedEntities);
return ResponseEntity.ok(true);
}
}
#Value
public class DispatchedRequest {
String id;
List<String>labsigned;
Long dateCreated;
String createdBy;
}
#Entity
#Table(name = "DISPATCHED")
#Data
#FieldDefaults(level = AccessLevel.PRIVATE)
public class DispatchedEntity {
String id;
String labsigned;
Long dateCreated;
String createdBy;
}
#Component
public class DispatchedEntityFactory {
public List<DispatchedEntity> creatMultipleFromDispatchRequest(final DispatchedRequest dispatchedRequest) {
List<DispatchedEntity> dispatchedEntities = new ArrayList<DispatchedEntity>();
for(String labsignature : dispatchedRequest.getLabsigned()) {
DispatchedEntity dispatchedEntity = new DispatchedEntity(dispatchedRequest.getId(),labsignature, dispatchedRequest.getDateCreated(), dispatchedRequest.getCreatedBy());
dispatchedEntities.add(dispatchedEntity);
}
return dispatchedEntities;
}
}

Why is hibernate creating null foreign keys?

I am trying to create a spring boot application with two entities: Question and QuestionChoices. I'm using a bidirectional onetomany relationship. When I try to create a Question entity along with a list of QuestionChoices, the foreign key in the QuestionChoice is coming out null.
Here is my QuestionChoice entity:
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
public class QuestionChoice {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String choice;
#ManyToOne
#JoinColumn(name = "question_id")
private Question question;
public QuestionChoice(String choice, Question question) {
this.choice = choice;
this.question = question;
}
public QuestionChoice(String choice) {
this.choice = choice;
}
}
Here is my Question entity:
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Question {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int question_id;
private String questionName;
private String questionText;
#OneToMany(mappedBy = "question", cascade = CascadeType.ALL)
private List<QuestionChoice> questionChoices;
public Question(String questionName, String questionText, List<QuestionChoice> questionChoices) {
this.questionName = questionName;
this.questionText = questionText;
this.questionChoices = questionChoices;
this.questionChoices.forEach(x -> x.setQuestion(this));
}
}
I have a QuestionRepository and QuestionChoiceRepository:
#Repository
public interface QuestionRepository extends JpaRepository<Question, Integer> {
}
#Repository
public interface QuestionChoiceRepository extends JpaRepository<QuestionChoice, Integer> {
}
Here is my controller:
#RestController
public class Controller {
QuestionRepository questionRepository;
QuestionChoiceRepository questionChoiceRepository;
public Controller(QuestionRepository questionRepository,
QuestionChoiceRepository questionChoiceRepository) {
this.questionRepository = questionRepository;
this.questionChoiceRepository = questionChoiceRepository;
}
#PostMapping("/question")
public Question createQuestion(#RequestBody Question question) {
return questionRepository.save(question);
}
#GetMapping("/question")
public List<Question> getQuestions() {
return questionRepository.findAll();
}
}
Here is my POST request:
POST http://localhost:8080/question
Content-Type: application/json
{
"questionName": "gender",
"questionText": "What is your gender?",
"questionChoices": ["male", "female"]
}
Here is the response from the POST:
{
"id": 1,
"questionName": "gender",
"questionText": "What is your gender?",
"questionChoices": [
{
"id": 1,
"choice": "male",
"question": null
},
{
"id": 2,
"choice": "female",
"question": null
}
]
}
And here is the response from the GET request:
GET http://localhost:8080/question
HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 16 Oct 2019 11:10:51 GMT
[
{
"id": 1,
"questionName": "gender",
"questionText": "What is your gender?",
"questionChoices": []
}
]
So not only are the foreign keys of the QuestionChoices null, but the list of question choices in the question entity is also null.
Any idea what I am doing wrong?
Update
I've found a good solution to this problem here: Infinite Recursion with Jackson JSON and Hibernate JPA issue. The problem is with Jackson, not Hibernate. Just add an additional annotation to the reference objects within the entities and everything works great!
You're sending an array of strings for your questionChoices in the JSON body. Your JSON mapper needs to populate a List<Question> from this array of strings. So it needs to transform each String into a QuestionChoice object. Presumably, it does that by calling the QuestionChoice constructor that takes a String as argument.
So you're saving a Question which has QuestionChoices which all have a null question property. So you're telling JPA that all QuestionChoices don't have any question (since it's null). So JPA saves what you tell it to save: QuestionChoices without any parent question.
You need to properly initialize the question property of the QuestionChoice.
The deserializer will always use the default constructor to construct the object. Your custom constructor has no effect over the deserialization.
What you can do is:
1 - Guarantee the association in your service / controller layer
#PostMapping("/question")
public Question createQuestion(#RequestBody Question question) {
question.getQuestionChoices().forEach(choice -> choice.setQuestion(question));
return questionRepository.save(question);
}
or 2 - Guarantee the association in your setter method:
public class Question {
// omitted for brevity
#OneToMany(mappedBy = "question", cascade = CascadeType.ALL)
private List<QuestionChoice> questionChoices;
public void setQuestionChoices(List<QuestionChoice> questionChoices) {
if (questionChoices != null) {
questionChoices.forEach(choice -> choice.setQuestion(this));
}
this.questionChoices = questionChoices;
}
}
Update
To prevent the infinite recursion, simply remove the 'question' attribute from the 'questionChoice' for presentation purposes.
I can think of two options:
1 - Set the question to null inside of questionChoice
#PostMapping("/question")
public Question createQuestion(#RequestBody Question question) {
Question savedQuestion = questionRepository.save(question);
savedQuestion.getQuestionChoices().forEach(choice -> choice.setQuestion(null));
return savedQuestion;
}
#GetMapping("/question")
public List<Question> getQuestions() {
List<Question> questions questionRepository.findAll();
questions.forEach(question -> {
question.getQuestionChoices.forEach(choice -> choice.setQuestion(null));
});
return questions;
}
This will save your question choices and foreign keys into the database, but will serialize questionChoices.question as null when sending the response to prevent infinite recursion.
2 - Use of DTOs.
You create a DTOs to serialize them as response objects to return exactly what you want to.
QuestionDTO.java
public class QuestionDTO {
private int question_id;
private String questionName;
private String questionText;
// notice that here you're using composition of DTOs (QuestionChoiceDTO instead of QuestionChoice)
private List<QuestionChoiceDTO> questionChoices;
// constructors..
// getters and setters..
}
QuestionChoiceDTO.java
public class QuestionChoiceDTO {
private int id;
private String choice;
// notice that you don't need to create the Question object here
// constructors..
// getters and setters..
}
Then in your controller:
#PostMapping("/question")
public QuestionDTO createQuestion(#RequestBody Question question) {
Question savedQuestion = questionRepository.save(question);
List<QuestionChoiceDTO> questionChoices = new ArrayList<>();
savedQuestion.getQuestionChoices().forEach(choice -> {
questionChoices.add(new QuestionChoiceDTO(choice.getId(), choice.getChoice()));
});
QuestionDTO response = new QuestionDTO(savedQuestion.getQuestion_id(), savedQuestion.getQuestionName(), savedQuestion.getQuestionText(), questionChoices);
return response;
}
#GetMapping("/question")
public List<QuestionDTO> getQuestions() {
List<Question> questions = questionRepository.findAll();
List<QuestionDTO> response = new ArrayList<>();
questions.forEach(question -> {
List<QuestionChoicesDTO> questionChoices = new ArrayList<>();
question.getQuestionChoices().forEach(choice -> questionChoices.add(new QuestionChoiceDTO(choice.getId(), choice.getChoice()));
responses.add(new QuestionDTO(savedQuestion.getQuestion_id(), savedQuestion.getQuestionName(), savedQuestion.getQuestionText(), questionChoices));
});
}
I always prefer the latter, because for big projects, IMHO, the use of DTO's can be a strong tool for organizing code and making concise use of request / response objects without using your domain objects.
You don't use your constructor public Question(...) after request. You should make a method to link choices with question

How to get username from mono<user> on spring boot webflux?

I try to make handler and router classes of spring boot webflux. The model class is user class. Codes are
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
#Document(collection="Users")
public class User {
#Id
private String _id;
#Indexed(unique = true)
private Long id;
#Indexed(unique=true)
private String username;
private String password;
private String email;
private String fullname;
private String role;
}
And below is the handler class of webflux project. In register method, I make the id duplication test codes. But It is totally WRONG.
#Component
public class UserHandler {
#Autowired
private UserReactiveMongoRepository userRepository;
public Mono<ServerResponse> register(ServerRequest request) {
Mono<User> monoUser = request.bodyToMono(User.class);
String id = monoUser.map(u -> u.get_id()).toString();
if(userRepository.existsById(id) == null)
return ServerResponse.ok().build(userRepository.insert(monoUser));
return ServerResponse.ok().build();
}
}
I want to extract the username or id string from Mono of spring webflux.
Any comments will be needed. I am stuck with this part.
One of the things which is wrong here is this like String id = monoUser.map(u -> u.get_id()).toString();. The toString will return you a String like "Mono#13254216541", because you are calling Mono.toString.
One more thing, you shouldn't use the request's data in the body of your function, but in a map or flatMap function.
You could replace it by something like (I'm doing it by head so it might not be 100% syntax corect):
Mono<User> userMono = request.bodyToMono(User.class);//Create a Mono<User>
userMono.map((user) -> { //In the map method, we access to the User object directly
if(user != null && user.getId() != null){
return userRepository.insert(user); // Insert User instead of Mono<User> in your repository
}
return null;
}) //This is still a Mono<User>
.map(insertedUser -> ServerResponse.ok(insertedUser)) //This is a Mono<ServerResponse>
.switchIfEmpty(ServerResponse.ok());
Hope this helps!
a more clean approach would be (i don't like the return of null in the map that others have done) is use the doOnSuccess:
request.bodyToMono(User.class)
.doOnSuccess(user -> return userRepository.insert(user))
.map(user -> ServerResponse.ok(user))
i have left out any error checks but ofc they should be done properly.

Rewrite code to get only several variables

I have this code which I would like to use to translate keys and return data to front end:
#GetMapping("pages")
public Page<ContractDTO> pagxes(#RequestParam(value = "page") int page, #RequestParam(value = "size") int size) {
return contractService.findAll(page, size)
//.map(mapper::toDTO);
.map(g -> new ContractDTO(g.getName(), getMerchantName(g.getMerchant_id())));
}
private String getMerchantName(int id) {
Optional<Merchants> obj = merchantService.findById(id);
return obj.get().getName();
}
DTO :
public class ContractDTO {
private Integer id;
.....
private Integer acquirer_id;
private Integer terminal_id;
private String merchant_id;
......
}
How I can rewrite this code .map(g -> new ContractDTO(g.getName(), getMerchantName(g.getMerchant_id()))); to translate from int to String using getMerchantName(int id) only terminal_id and merchant_id and all other variables not to be translated?
I can create constructor in ContractDTO but the code will be huge. Is there some other way?
Error:
The method builder() is undefined for the type ContractDTO
In your case because you want to avoid multiple constructors, You can use a builder design pattern, by using lombok library, it can be more easier, so you can just annotate your class of ContractDTO with this library annotation, and you have every thing to go :
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#Builder(toBuilder = true)
class ContractDTO {
private Integer id;
private String name;
private Integer acquirerId;
private Integer terminalId;
private String merchantId;
}
then your code can be :
...
.map(g -> ContractDTO.builder()
.name(g.getName())
.merchantName(g.getMerchantId())
.build()
)....

Categories

Resources