Getting multiple Mono objects with reactive Mongo queries - spring

I'm using the webflux framework for spring boot, the behavior I'm trying to implement is creating a new customer in the database, if it does not already exist (throw an exception if it does)
and also maintain another country code database (if the new customer is from a new country, add to the database, if the country is already saved, use the old information)
This is the function in the service :
public Mono<Customer> createNewCustomer(Customer customer) {
if(!customer.isValid()) {
return Mono.error(new BadRequestException("Bad email or birthdate format"));
}
Mono<Customer> customerFromDB = customerDB.findByEmail(customer.getEmail());
Mono<Country> countryFromDB = countryDB.findByCountryCode(customer.getCountryCode());
Mono<Customer> c = customerFromDB.zipWith(countryFromDB).doOnSuccess(new Consumer<Tuple2<Customer, Country>>() {
#Override
public void accept(Tuple2<Customer, Country> t) {
System.err.println("tuple " + t);
if(t == null) {
countryDB.save(new Country(customer.getCountryCode(), customer.getCountryName())).subscribe();
customerDB.save(customer).subscribe();
return;
}
Customer cus = t.getT1();
Country country = t.getT2();
if(cus != null) {
throw new CustomerAlreadyExistsException();
}
if(country == null) {
countryDB.save(new Country(customer.getCountryCode(), customer.getCountryName())).subscribe();
}
else {
customer.setCountryName(country.getCountryName());
}
customerDB.save(customer).subscribe();
}
}).thenReturn(customer);
return c;
}
My problem is, the tuple returns null if either country or customer are not found, while I need to know about them separately if they exist or not, so that I can save to the database correctly.
country == null is never true
I also tried to use customerFromDB.block() to get the actual value but I receive an error that it's not supported, so I guess that's not the way
Is there anyway to do two queries to get their values?

Solved it with the following solution:
public Mono<Customer> createNewCustomer(Customer customer) {
if(!customer.isValid()) {
return Mono.error(new BadRequestException("Bad email or birthdate format"));
}
return customerDB.findByEmail(customer.getEmail())
.defaultIfEmpty(new Customer("empty", "", "", "", "", ""))
.flatMap(cu -> {
if(!cu.getEmail().equals("empty")) {
return Mono.error(new CustomerAlreadyExistsException());
}
return countryDB.findByCountryCode(customer.getCountryCode())
.defaultIfEmpty(new Country(customer.getCountryCode(), customer.getCountryName()))
.flatMap(country -> {
customer.setCountryName(country.getCountryName());
customerDB.save(customer).subscribe();
countryDB.save(country).subscribe();
return Mono.just(customer);});
});
}
Instead of doing both queries simulatneaously, I queried for one result and then queries for the next, I think this is the reactive way of doing it, but I'm open for corrections.

Related

return Mono.error inside Map Spring Webflux

For Matching condition, return Mono.error from code , but gives compilation error. I commented out
//return Mono.error(new RuntimeException("User Phone Exists already"));
Result:
Compilation Error: Required Type Mono<EventSlotBook>, Provided
Mono<Object>
Code:
public Mono<EventSlotBook> getEventSlotBookWithAppointmentId(EventSlotBook eventSlotBookEntity) {
Query query = new Query();
query.addCriteria(
new Criteria().andOperator(
Criteria.where("eventId").is(eventSlotBookEntity.getEventId()),
Criteria.where("eventConfigId").is(eventSlotBookEntity.getEventConfigId()),
Criteria.where("eventSlotId").is(eventSlotBookEntity.getEventSlotId())));
return this.reactiveMongoTemplate.findOne(query, EventSlotBook.class)
.map(eventSlotBookEntityFromDb -> {
EventSlotBook eventSlotNewEntity = eventSlotBookEntityFromDb.toBuilder().build();
if(eventSlotNewEntity.getEventUsers() != null) {
for(EventUser eventUserIno:eventSlotNewEntity.getEventUsers()) {
if(eventUserIno.getPhoneNumber().equalsIgnoreCase(eventSlotBookEntity.getEventUser().getPhoneNumber())){
//return Mono.error(new RuntimeException("User Phone Exists already"));
}
}
}
int maxTokenVal = Integer.valueOf(eventSlotNewEntity.getMaxTokenVal()).intValue() + 1;
EventUser eventUser = new EventUser(eventSlotNewEntity.getEventUser().getName(),eventSlotNewEntity.getEventUser().getPhoneNumber(),String.valueOf(maxTokenVal));
eventSlotNewEntity.getEventUsers().add(eventUser);
eventSlotNewEntity.setMaxTokenVal(String.valueOf(maxTokenVal));
eventSlotNewEntity.setEventUser(eventUser);
return eventSlotNewEntity;
//return Mono.error(new RuntimeException("Ts"));
})
.switchIfEmpty(getEventSlotBook(eventSlotBookEntity));
}
caller of Method : I should handle Mono.error and return to rest API that user already exists ?. Please help on this
public Mono<EventSlotBookRequestDto> saveEventSlotBook(Mono<EventSlotBookRequestDto> eventSlotBookRequestDtoMono){
log.info("Start::SaveEventSlotBook");
Mono<EventSlotBookRequestDto> eventDtoSaved =
eventSlotBookRequestDtoMono.map(AppUtils::dtoToEntity)
.flatMap(eventSlotEntity -> getEventSlotBookWithAppointmentId(eventSlotEntity))
.doOnNext(eventSlotEntityBeforeSave -> {
log.info("####BeforeSave::{}",eventSlotEntityBeforeSave);
})
.flatMap(eventSlotBookrepository::save)
.doOnNext( eventSlotBookAfterSave -> {
log.info("####AfterSave::{}",eventSlotBookAfterSave);
})
.map(AppUtils::entityToDto);
log.info("End::SaveEventSlotBook");
return eventDtoSaved;
}
map is used to apply a synchronous function to each item, therefore you can't return Mono from it. To return an error from map you could just throw an exception and error signal will be emited. As an alternative you you use handle operator and use SynchronousSink to emit next or error signal.
But in your case you need to use flatMap instead because saveEventSlotBook returns Mono and should be transformed asynchronously.

Spring Data JPA : Efficient Way to Invoke Repository Methods with Optional Parameters

I have the below Java 11 method which is invoked by the controller where ID is the required param and status,version are optional params. I had to write multiple repository methods to fetch the record based on those params. Am wondering is there a better/effiecient way to refactor this method with out the if/else ladder?
#Override
#Transactional(transactionManager = "customTransactionManager")
public Optional<String> getInformation(UUID id, Status status, Long version) {
try {
Preconditions.checkNotNull(id, ID_MUST_BE_NOT_NULL_MSG);
if (status != null && version != null) {
return repository.findByIdAndVersionAndStatus(id, version, status);
} else if (status != null) {
return repository.findFirstByIdAndStatus(id, status);
} else if (version != null) {
return repository.findFirstByIdAndVersion(id, version);
} else {
return repository.findFirstByIdOrderByIdDesc(id);
}
} catch (Exception e) {
log.error(e);
throw new CustomException(MessageFormat.format(PUBLIC_ERROR_MESSAGE, id));
}
}
You could use Specifications for that:
private Specification<YourEntity> toSpecification(UUID id, Status status, Long version) {
return (root, query, builder) -> {
Set<Predicate> predicates = new HashSet<>();
predicates.add(builder.equal(root.get("id"), id));
if (status != null) predicates.add(builder.equal(root.get("status"), status));
if (version != null) predicates.add(builder.equal(root.get("version"), version));
return builder.and(predicates.toArray(Predicate[]::new));
};
}
If you let your repository extend JpaSpecificationExecutor you can use the build specification object like so:
Specification<YourEntity> specification = toSpecification(id, status, version);
Optional<YourEntity> result = repository.findOne(specification);
When using Hibernate Metamodel Generator you can also write builder.equal(YourEntity_.id, id) instead of builder.equal(root.get("id"), id).
In addition to the accepted answer, I find Query By Examples much more intuitive and simple.
https://www.baeldung.com/spring-data-query-by-example would be a good start.
It basically creates a query based on non-null fields from your jpa entity.

How to define dependencies on two client calls in quarkus reactive programming

I have two Client APIs that return an Uni.
Uni<Customer> getCustomer(customerID)
Uni<Address> getAddress(addressID)
And I want to open a REST API
Uni<FullCustomer> getFullCustomer(String customerID)
The logic is to make the Customer Client call first. If the returned customer object has addressID then make the second Address Client call and get shipping address details. If shipping address is not available then just wrap the customer in FullCustomer object and return else wrap both customer and address in FullCustomer object and return.
I dont want to block the thread on client call (await().indefinitely()), hence i am using onItem and transfer method call. But my code returns a Uni<Uni> and i want it to return a Uni.
#GET
#Path("api/customer/{id}")
#Produces({ "application/json" })
Uni<Uni<FullCustomer>> getFullCustomer(#PathParam("id") String customerID){
Uni<Customer> customerResponse = getCustomer(customerID);
Uni<Uni<FullCustomer>> asyncResponse = customerResponse.onItem().transform(customer -> {
if (customer.getAddressId() != null) {
Uni<Address> addressResponse = getAddress(customer.getAddressId());
Uni<FullCustomer> fullCustomer = addressResponse.onItem().transform(address -> {
if (address.getShippingAddress() != null) {
return new FullCustomer(customer, address.getShippingAddress());
} else {
return new FullCustomer(customer);
}
});
}
return Uni.createFrom().item(new FullCustomer(customer));
});
return asyncResponse;
}
How can I rewrite my code so that it returns Uni keeping reactive ( async client ) calls
Got the solution. Thanks Ladicek for comments.
public Uni<FullCustomer> getFullCustomer(#PathParam("id") String customerID) {
return getCustomer(customerID)
.onItem()
.transformToUni(customer -> {
if (customer.getAddressId() != null) {
return getAddress(customer.getAddressId()).onItem().transform(address -> {
if (address.getShippingAddress() != null) {
return new FullCustomer(customer, address.getShippingAddress());
} else {
return new FullCustomer(customer);
}
});
} else {
return Uni.createFrom().item(new FullCustomer(customer));
}
});
}

calling my apex method in apex trigger getting the error

public static void insertInboundJive(Map<Id, String> mapCases){
try{
system.debug('Aditya');
Map<Id, String> mapCases1 = new Map<Id, String>();
Map<Id, Integer> mapIncrements = new Map<Id, Integer>();
//List<ICS_Case_Interaction__c> lstCaseInteraction;
if(mapCases != null && mapCases.size() > 0) {
List<ICS_Case_Interaction__c> lstCaseInteraction = [ SELECT Id,case__r.origin FROM ICS_Case_Interaction__c Where case__r.Id =:mapCases.keySet()];
for(ICS_Case_Interaction__c caseInteracts :lstCaseInteraction ){
if(caseInteracts.case__r.Id != null && caseInteracts.case__r.Status == 'New Customer Message'){
system.debug('**AdityaDebug**' +caseInteracts.case__r.Id);
system.debug('**AdityaDebug**' +caseInteracts.case__r.Status);
mapcases1.put(caseInteracts.case__r.Id , TYPE_JIVE_INBOUND);
Integer intIncrement = mapIncrements.get(caseInteracts.case__r.Id);
system.debug('Increment' +intIncrement);
if(intIncrement != null){
intIncrement++;
system.debug('Increment++' +intIncrement);
}
else {
intIncrement = 1;
}
mapIncrements.put(caseInteracts.case__r.Id, intIncrement);
}
}
if(mapCases.size() > 0) {
insertByCaseAsync(mapCases, mapIncrements);
}
}
}
catch(Exception ex){
Core_Log_Entry.logEntryWithException('Case Interaction Metrics', 'CaseInteraction','insertInboundEmail', 'Error', null, null, ex);
}
}
This is my Method in the class.I am trying to call the apex method in the trigger.but its throwing the error.Could you please help me and try to reach out the best.
The error which I am getting was
line 188, col 106. Method does not exist or incorrect signature: void insertInboundJive(List) from the type ICS_Case_Interactions_Trigger_Handler
if(trigger.isUpdate) {
if(Label.ICS_Case_Interaction_Metrics.equals('1')) {ICS_Case_Interactions_Trigger_Handler.insertInboundJive(trigger.new);}
}
You are trying to pass the wrong parameters. In the method you have defined that when called you need to pass a Map where the values are String however you are passing Trigger.new which is a list of Objects. My approach is to handle the mapping in the trigger and then manipulate data in the controller:
In this case you can do the below to pass the records and get the string of data you want in the controller.. or do it in the trigger so you don't change the controller.
Map<Id,Contact> map = new Map<Id,ICS_Case_Interaction__c>(); // new map
for(ICS_Case_Interaction__c con :trigger.new){
map.put(con.Id, con); // enter the records you need for the method
}
if(trigger.isUpdate) {
if(Label.ICS_Case_Interaction_Metrics.equals('1')) {
ICS_Case_Interactions_Trigger_Handler.insertInboundJive(map);
}
}
and in the controller you should have
public static void insertInboundJive(Map<Id, ICS_Case_Interaction__c> mapCases){
}

Relay nodeDefinition throws "Right-hand side of 'instanceof' is not callable"

I am learning GraphQL and have just started implementing graphql-relay on my node server in the past couple days. When declaring my Relay node definitions I am getting the error "Right-hand side of 'instanceof' is not callable", where the right-hand side is an object with a constructor function. As far as I can tell this is not due to improper usage, also considering this is copied from the docs. I am not sure what the expected outcome is when it is properly working, I assume it returns the GraphQL type to be put through the works and have the requested data returned.
var {nodeInterface, nodeField} = nodeDefinitions(
(globalId) => {
var {type, id} = fromGlobalId(globalId);
console.log(type);
if (type === 'User') {
return db.models.user.findById(id)
} else if (type === 'Video') {
return db.models.video.findById(id)
}
else if (type === 'Producer') {
return db.models.user.findById(id)
}
else if (type === 'Viewer') {
return db.models.user.findById(id)
}else {
return null;
}
},
(obj) => {
console.log(obj); // Sequelize object
console.log(User); // user
console.log(User.constructor); // valid constructor
// This is where the error occurs
if (obj instanceof User) {
return UserType;
} else if (obj instanceof Video) {
return VideoType;
} else {
return null;
}
});
Notes:
Using Sequelize ORM.
User is an Interface in GraphQL the schema implemented by Viewer, Producer, and GeneralUser types. My psql database on the otherhand has one User table which is why the second function only checks for User and not these additional types.
All my other queries for users, videos, etc. work fine, it is only when searching by node && globalId when it breaks
An "object with a constructor function" isn't callable. Probably you need to make that object a class with a constructor like this:
class User {
constructor(id, name, email) {
this.id = id;
// etc
}
}
You need to change the code from:
...
if (obj instanceof User) {
...
} else if (obj instanceof Video) {
....
to:
...
if (obj instanceof User.Instance) {
...
} else if (obj instanceof Video.Instance) {
....

Resources