I'm working on microservice based Java application using JDK 1.8, Spring 5 and SpringBoot 2.0. I'm using JPA in application for CRUD. I have following method to update an existing record with cart id in database (PostGres) :-
#Override
#CachePut(value = CommerceConnectorConstants.CommerceCache.COMMERCE_CACHE_TENANT_USER_DATA_MAP)
public Mono<CommerceTenantUser> updateCartId(String tenantId, String userId, String cartId) {
logger.info("cartId->"+cartId);
final TenantUserKey commerce = getCommerceObj(tenantId, userId);
Optional <CommerceTenantUser> tenantUser = commerceTenantUserRepository.findById(commerce);
return tenantUser.map(tenantUserObj -> updateCartIdAndRefreshMap(tenantUserObj, cartId)).orElse(Mono.empty());
}
private Mono<CommerceTenantUser> updateCartIdAndRefreshMap(CommerceTenantUser tenantUserObj, String cartId) {
tenantUserObj.setCartId(cartId);
final Mono<CommerceTenantUser> commerceTenantUser = asyncRunner
.one(() -> commerceTenantUserRepository.saveAndFlush(tenantUserObj))
.doOnNext(value -> commerceCacheService.refreshMap())
.doOnError(error -> logger.error("Error while persisting Cart Id: {}", error))
.map(commerceTenantUserObj -> commerceTenantUserObj);
return commerceTenantUser;
}
My issue is in one of the application flow, I'm unable to update the cart id. I tried to debug the flow many times, I can clearly see that the call is happening to this method and I can log cart id value in console as well but in the end the update of cart id never actually happens in database.
Please note this same method for updating the cart id is happening in other flows of application where cart id eventually gets saved in database (except my flow) so I guess there is no issue with the implementation logic of saving.
Following is the flow where this method invocation is happening but not saving in database :-
#Override
public Mono verifyCredentialsforBasicAuthorization(Map requestInfo) {
Tuple4<String, String, WebClient, String> serviceConnectionDetails = commerceConnectorHelper.verifyCredentialsforBasicAuthorization(requestInfo);
if(!StringUtils.isBlank(serviceConnectionDetails._1)) {
logger.info("Calling cart id update service...");
return serviceConnectionDetails._3
.post()
.uri(builder -> builder.path(serviceConnectionDetails._1)
.queryParam(CommerceConnectorConstants.CartParams.OLD_CART_ID, serviceConnectionDetails._4)
.build())
.accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
.retrieve()
.bodyToMono(CartModel.class)
.map(response ->
{
return repositoryDetails.updateCartId(requestInfo.get(CommerceConnectorConstants.HttpHeaders.TENANT_ID).get(),
requestInfo.get(CommerceConnectorConstants.HttpHeaders.TENANT_USER_ID).get(), response.getGuid()).then(Mono.just(response));
}) .then(repositoryDetails.updateCommerceUserId(requestInfo.get(CommerceConnectorConstants.HttpHeaders.TENANT_ID).get(),
requestInfo.get(CommerceConnectorConstants.HttpHeaders.TENANT_USER_ID).get(),
requestInfo.get(CommerceConnectorConstants.AuthorizationParams.AUTHORIZATION_USERNAME).get()))
.then(serviceConnectionDetails._3
.get()
.uri(serviceConnectionDetails._2)
.accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
.retrieve()
.bodyToMono(UserProfile.class)
.doOnNext(value -> logger.info("Called User Profile service {}", value.getCustomerId()))
.doOnError(error -> logger.error("Error while calling User Profile service: {}", error)));
}
else
//Update Commerce User Id
;
//User Profile Call
return serviceConnectionDetails._3
.get()
.uri(serviceConnectionDetails._2)
.accept(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)
.retrieve()
.bodyToMono(UserProfile.class)
.doOnNext(value -> logger.info("Called User Profile service {}", value.getCustomerId()))
.doOnError(error -> logger.error("Error while calling User Profile service: {}", error));
}
If you notice, I'm calling this method repositoryDetails.updateCartId and passing all the required parameters including the cart id by using response.getGuid(). The method is executed in the end but eventually it does not saves the data in table. I tried really hard but not able to figure out the issue. This is the problem with asynchronous calls using the Java Reactive programming. I would really appreciate if you can help me in figuring out issue. Thanks
I found the root cause of it. I was suppose to call update once only on same Entity using JPA. In my case I was calling twice individually to update 2 different columns in same Entity.
Related
I am fairly new to Java and Spring Boot (coming from TypeScript) and
experimenting with a small restful CRUD Controller using the
reactive Spring Boot API.
There are many tutorials and examples out there but they all lack
proper response statuses, e.g. giving a 404 on DELETE when the
resource doesn't exist.
What I like to achieve is a DELETE handler which
returns "204 No Content" if the resource existed and was deleted successfully
returns "404 Not found" if the resource doesn't exist
A simple "I don't care about HTTP status" DELETE handler
looks like this:
#DeleteMapping("/{id}")
public Mono<Void> deletePet(#PathVariable String id) {
return petRepository.deleteById(id);
}
This always gives status 200, even when there is no Pet for this ID.
I tried to use petRepository.findById(id) and .defaultIfEmpty()
in several ways to catch the 404 case, but without luck. E.g. with
this implementation I am getting always 204:
#DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deletePet(#PathVariable String id) {
return petRepository.findById(id)
.map(pet1 -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT))
.defaultIfEmpty(new ResponseEntity<Void>(HttpStatus.NOT_FOUND))
.flatMap(res -> {
return petRepository.deleteById(id)
.map(v -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT));
});
}
I think I understand why this isn't working, because after the .defaultIfEmpty()
the Mono isn't empty anymore and the .flatMap will have something to work
on (the 404 response) so the deleteById() is executed. This returns an (obviously)
non empty Mono as well, so the status turns into NO_CONTENT again.
But all my (many) attempts to change this failed so I hope anyone has the right
solution for this problem.
Thanks! :)
When findById returns an empty Mono, the code below will not executed either map or flatMap and will only return the value from defaultIfEmpty
#DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deletePet(#PathVariable String id) {
return petRepository.findById(id)
.map(pet1 -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT))
.flatMap(res -> {
return petRepository.deleteById(id)
.map(v -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT));
})
.defaultIfEmpty(new ResponseEntity<Void>(HttpStatus.NOT_FOUND));
}
Also, your understanding as to why this happens in your code snippet is correct.
After some more research I found a solution:
#DeleteMapping("/{id}")
#ResponseStatus(HttpStatus.NO_CONTENT)
public Mono<ResponseEntity<Void>> deletePet(#PathVariable String id) {
return petRepository.findById(id)
.defaultIfEmpty(new Pet())
.flatMap(pet -> {
if (null == pet.id) {
return Mono.just(new ResponseEntity<Void>(HttpStatus.NOT_FOUND));
}
else {
return petRepository.deleteById(id)
.map(v -> new ResponseEntity<Void>(HttpStatus.NO_CONTENT));
}
});
}
Now an empty Pet object is created when findById() gives an empty
result using defaultIfEmpty().
So the flatMap() gets either the Pet for the given ID or an empty
Pet. The latter is recognized by the fact that the id property is null
which is turned into a 404 response. In the other case the Pet is
deleted and 204 is returned. But note, that the .map() there isn't executed because of the empty deleteById() result. It's just necessary to satisfy the generic interface here. The 204 comes from the #ResponseStatus annotation.
So this is a possible solution - but it looks not very elegant to me (creating an empty Pet and having this no-op deleteById().map()).
If there is a better way to do this, please give your answer here.
I'm using WebFlux, and I want to log with AOP as follow:
Class LogAop {
#AfterReturning("execution ...")
public void configSetted() {
// Getting username by token
Mono<Sting> username = ReactiveSecurityContextHolder.getContext()
.map { ... };
username.subscribe ({it -> loggerin.info(it);});
}
}
In the above code, I want to log username, but there is no log. How can I subscribe to Mono or Flux without returning to the method?
Note: Some times I want to do differnt things on subscribe data, such as saving data in db.
This should work unless ReactiveSecurityContextHolder.getContext() doesn't emit any item.
Mono<String> username = ReactiveSecurityContextHolder.getContext()
.map { ... };
username.subscribe (it -> loggerin.info(it));
I'd suggest adding a log to find out:
username.log().subscribe (it -> loggerin.info(it));
I am attempting an application in JHipster 7.0.1 (Azul JDK 11) and ReactJS as the front-end.
I have 2 entities in my JDL - Domain and BadgeCategory that are related as shown below
relationship OneToMany {
Domain{badgeClass required} to BadgeCategory{domain(name) required}
I want to be able to display all the BadgeCategories for a particular Domain in the Domain Detail screen.
For this, I created a new method in the repository BadgeCategoryRepository.java
#Repository
public interface BadgeCategoryRepository extends JpaRepository<BadgeCategory, Long> {
List<BadgeCategory> findByDomainId(Long id);
}
And then added a new endpoint in BadgeCategoryResource.java
#GetMapping("/badge-categories-domain/{id}")
public ResponseEntity<List<BadgeCategory>> getAllBadgeCategoriesForDomain(#PathVariable Long id) {
log.debug("REST request to get BadgeCategories for Domain : {}", id);
List<BadgeCategory> badgeCategory = badgeCategoryRepository.findByDomainId(id);
return ResponseEntity.ok().body(badgeCategory);
}
Now coming to the React part, I added a constant in badge-category.reducer.ts
export const getEntitiesForDomain = createAsyncThunk(
'badgeCategory/fetch_entity_list_for_domain',
async (id: string) => {
const requestUrl = `api/badge-categories-domain/${id}`;
alert(JSON.stringify(axios.get<IBadgeCategory[]>(requestUrl)));
return axios.get<IBadgeCategory[]>(requestUrl);
});
Then I am using this reducer in the Domain Detail screen component domain-detail.tsx
import { getEntity as getBadgeCategory, getEntitiesForDomain } from '../badge-category/badge-category.reducer';
const domainEntity = useAppSelector(state => state.domain.entity);
const badgeCategoryList = useAppSelector(state => state.badgeCategory.entities);
useEffect(() => {
dispatch(getEntity(props.match.params.id));
dispatch(getEntitiesForDomain(props.match.params.id));
}, []);
I am expecting the constant badgeCategoryList to contain the list of all badge categories for the domain which is being referred to in the domain-detail screen. But I get nothing in return.
On checking the flow, I see that the endpoint is getting hit and the response is being produced by the Java code, but the UI code is not able to consume it.
What am I missing here that is causing this issue?
The Swagger docs show expected response from the Java code
So the issue was with the new API call not being registered with the slice section in the reducer. I had to do the following addition to the slice and it works like a charm
.addMatcher(isFulfilled(getEntitiesForDomain), (state, action) => {
return {
...state,
loading: false,
entities: action.payload.data,
};
})
I am newbie at Caffeine and Spring boot. I am trying to find a solution to cache a list of objects. the DB transaction takes 3 seconds to load the list, so I am looking to cache the resultset using Caffeine. To cache each item individually, I am following the below approach, but it doesn't seem to work.
public List<Item> getAllItemsOnStartUp() {
allItemsList = repo.getAllItems();
for (Item item : allItemsList) {
staticItem = item;
getItem(item.getItmsId());
}
return allItemsList;
}
#CachePut(value = "allItems", key = "#itmsId")
public Item getItem(BigInteger itmsId) {
return item;
}
#Override
#Cacheable(value = "allItems")
public List<Item> getAllItems() {
allItemsList = repo.getAllItems();
return allItemsList;
}
#Override
#Transactional
#CachePut(value = "allItems", key="#a0.itmsId")
public Item insertItem(Item item) {
Item savedItem = rRepo.save(item);
return savedItem;
}
When the server starts up, getAllItemsOnStartUp() must run and populate cache. the app calls getAllItems() to retrieve the list, it is expected to use cache here but every time the app gets data from DB which takes 3 seconds.
I saw this post today but I will answer anyway. Maybe it can help others.
Spring-boot uses by default CaffeineCache to cache service calls and it's very simple to use it. You just have to annotate your service method using #Cacheable. The example below caches the user permissions in a cache named "sso-users-cache" (because I don't want to call the service to check user's permissions all the time), creating an entry using company name (the system is multi-company) and userId, unless the method returns an error (remember: if you don't have a clausule unless, you can cache even an error of your service).
#Cacheable(cacheNames = ["sso-users-cache"], key = "#company.concat('-sso-user-').concat(#userId)", unless = "#result instanceof T(com.github.michaelbull.result.Err)")
fun fetchActionsByOrganizationAndUser(company: String, userId: String): Result<Set<String>, String> {
val response = Unirest
.get(getUserPermitionsUrl(company = company, userId = userId))
.header("Content-Type", "application/json")
.asString()
.ifFailure {
logger.error("SSO Error: Get user permissions failed: context: $company : userId: $userId")
}
return if(response.isSuccess) {
Ok(serializeUtil.asCollection(response.body, String::class.java).toSet())
} else {
Err("SSO Error: Get user permissions failed: context: $company : userId: $userId\"")
}
}
The parameter cacheNames defines an entry in your cache while the key, will tell the name of the entry on cache (it's used because users have different permissions and they need different entries inside the cache.
The parameter unless tells the cache to not cache if my method returns an error (very important!)
About update the cache informations, it's not necessary. What you need is to invalidate the cache information in case user's permission changes. For exemple, the method below add a new permission for the user and I need to invalidate (clear) the cache I have for the specific user:
#CacheEvict(cacheNames = ["sso-user-actions-cache"], key = "#company.concat('-user-actions-').concat(#userId)")
fun addPermissionToUser(company: String, userId: String, permission: String) {
Unirest
.put(addUserPermitionsUrl(company = company, userId = userId, permission = permission))
.header("Content-Type", "application/json")
.asEmpty()
}
Again: it's important to use the property key because I want to clear only the entry for a specific user!!! Otherwise, you will clean the whole permission cache (for all users)
After the #CacheEvict, the next call to check user's permission, spring will realize that the user doesn't have an entry on cache and will call the service and cache the return. That's it.
I have the following request handler
fun x(req: ServerRequest) = req.toMono()
.flatMap {
...
val oldest = myRepository.findOldest(...) // this is the object I want to modify
...
val v= anotherMongoReactiveRepository.save(Y(...)) // this saves successfully
myRepository.save(oldest.copy(
remaining = (oldest.remaining - 1)
)) // this is not saved
ok().body(...)
}
and the following mongodb reactive repository
#Repository
interface MyRepository : ReactiveMongoRepository<X, String>, ... {
}
The problem is that after the save() method is executed there is no changed in the object. I managed to fix the problem with save().block() but I don't know why the first save on the other repository works and this one isn't. Why is this block() required?
Nothing happens until someone subscribes to reactive Publisher. That's why it started to work when you used block(). If you need to make a call to DB and use the result in another DB request than use Mono / Flux operators like map(), flatMap(),... to build a pipeline of all the operations you need and after that return resulting Mono / Flux as controller’s response. Spring will subscribe to that Mono / Flux and will return the request. You don't need to block it. And it is not recommended to do it (to use block() method).
Short example how to work with MongoDB reactive repositories in Java:
#GetMapping("/users")
public Mono<User> getPopulation() {
return userRepository.findOldest()
.flatMap(user -> { // process the response from DB
user.setTheOldest(true);
return userRepository.save(user);
})
.map(user -> {...}); // another processing
}