I am struggling to find a list of jpa-s in my Spring web app by using the PagingAndSortingRepository but i always get some kind of an exception.
(my userRepository extends JpaRepository which extends PagingAndSortingRepository)
This is my code :
#RestController
#RequestMapping(value = "")
public class UsersController {
...
#RequestMapping(method=RequestMethod.GET, value = "/view-u", produces = MediaType.APPLICATION_JSON_VALUE)
#PreAuthorize("hasRole(T(com.myapp.util.Um.Privileges).CAN_USER_READ)")
public #ResponseBody List<JsonUser> getUsersPaginated(){
List<JsonUser> result = new ArrayList<JsonUser>();
result.addAll(dashboardServiceImpl.findUsersPaginatedAndSorted());
return result;
}
...
}
and service :
#Service
public class DasboardServiceImpl implements DashboardService /*My interface*/{
...
#Override
public List<JsonUser> findUsersPaginatedAndSorted(){
List<JsonUser> modelList = new ArrayList<JsonUser>();
final List<UserJPA> jpaList = userRepository.findAll(new PageRequest(0, 10, Sort.Direction.ASC, "username")).getContent();
for(final UserJPA jpa : jpaList){
modelList.add(new JsonUser(jpa));
}
return modelList;
}
}
This doesn't work. It throws the exception :
Unknown name value [] for enum class [com.health.common.enums.Gender];
nested exception is java.lang.IllegalArgumentException: Unknown name
value [] for enum class [com.health.common.enums.Gender]
I attest that the Gender enum works perfectly in every other database communication.
I then tried doing this to the service method :
#Override
public List<JsonUser> findUsersPaginatedAndSorted(){
List<JsonUser> modelList = new ArrayList<JsonUser>();
try{
// First call -> enum exception
userRepository.findAll(new PageRequest(0, 10, Sort.Direction.ASC, "username"));
}catch(Exception e){
System.out.println("\n\t"+e.getMessage()+"\n");
}
// Second call - collection exception
final List<UserJPA> jpaList = userRepository.findAll(new PageRequest(0, 10, Sort.Direction.ASC, "username")).getContent();
for(final UserJPA jpa : jpaList){
modelList.add(new JsonUser(jpa));
}
return modelList;
}
And this time, i get a different exception on the second findAll call :
org.springframework.orm.jpa.JpaSystemException: collection is not
associated with any session; nested exception is
org.hibernate.HibernateException: collection is not associated with
any session
After a lot of digging and frustration, i found the only solution which works for me :
I add #Transactional(readOnly=true) to the service method and catch different exceptions in both service and controller :
controller :
#RestController
#RequestMapping(value = "")
public class UsersController {
...
#RequestMapping(method=RequestMethod.GET, value = "/view-u", produces = MediaType.APPLICATION_JSON_VALUE)
#PreAuthorize("hasRole(T(com.health.common.util.Um.Privileges).CAN_USER_READ)")
public #ResponseBody List<JsonUser> getUsersPaginated(){
List<JsonUser> result = new ArrayList<JsonUser>();
// i have no idea why
try{
// when called the first time, it throws "Transaction marked as rollbackOnly"
result.addAll(dashboardServiceImpl.findUsersPaginatedAndSorted());
}catch(Exception e){
// and when called the second time, it works
result.addAll(dashboardServiceImpl.findUsersPaginatedAndSorted());
}
return result;
}
...
}
( Notice the exception i'm getting on the first service method call is :
TransactionSystemException : Could not commit JPA transaction; nested
exception is javax.persistence.RollbackException: Transaction marked
as rollbackOnly
)
And service :
#Service
public class DasboardServiceImpl implements DashboardService{
...
#Override
#Transactional(readOnly=true)
public List<JsonUser> findUsersPaginatedAndSorted(){
List<JsonUser> modelList = new ArrayList<JsonUser>();
// I have no idea why
try{
// when called the first time it throws (GenderEnum) exception
userRepository.findAll(new PageRequest(0, 10, Sort.Direction.ASC, "username"));
}catch(Exception e){
System.out.println("\n\t"+e.getMessage()+"\n");
}
// and when called the second time, it works
final List<UserJPA> jpaList = userRepository.findAll(new PageRequest(0, 10, Sort.Direction.ASC, "username")).getContent();
for(final UserJPA jpa : jpaList){
modelList.add(new JsonUser(jpa));
}
return modelList;
}
}
As i said, this works for me and i get a proper list of users, but this solution is far from elegant. Notice that i am doing a double call of both service method and of repository method. :(
How can i fix my code so i don't have to do these double calls ?
Why doesn't my original solution work ? Why does findAll(Paginated) requires #Transactional when it is a query that doesn't alter the database ?
Sorry for the long post and thank you in advance.
Related
I've followed an open Course on Spring web. Written some code to list all orders from a database and return them through a rest api. This works perfectly. Now I'm writing some code to give the ID of the order in the request, find 0 or 1 orders and return them. However, when there is no Order find with the given ID, a nullpointerexception is given. I can't find out what is causing this. I'm assuming the .orElse(null) statement. Please advise
Controller:
#RequestMapping("api/V1/order")
#RestController
public class OrderController {
private final OrderService orderService;
#Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
#GetMapping(path = "{id}")
public Order getOrderById(#PathVariable("id") int id) {
return orderService.getOrderById(id)
.orElse(null);
}
}
Service:
#Service
public class OrderService {
private final OrderDao orderDao;
#Autowired
public OrderService(#Qualifier("oracle") OrderDao orderDao) {
this.orderDao = orderDao;
}
public Optional<Order> getOrderById(int orderNumber) {
return orderDao.selectOrderById(orderNumber);
}
}
Dao:
#Override
public Optional<Order> selectOrderById(int searchedOrderNumber) {
final String sql = "SELECT \"order\", sender, receiver, patient, orderdate, duedate, paymentref, status, netprice from \"ORDER\" where \"order\" = ?";
Order order = jdbcTemplate.queryForObject(sql, new Object[] {searchedOrderNumber}, (resultSet, i) -> {
int orderNumber = resultSet.getInt( "\"order\"");
String sender = resultSet.getString("sender");
String receiver = resultSet.getString("receiver");
String patient = resultSet.getString("patient");
String orderDate = resultSet.getString("orderdate");
String dueDate = resultSet.getString("duedate");
String paymentRef = resultSet.getString("paymentref");
String status = resultSet.getString("status");
int netPrice = resultSet.getInt("netprice");
return new Order(orderNumber,sender,receiver,patient,orderDate,dueDate,paymentRef,status,netPrice);
});
return Optional.ofNullable(order);
}
For the Jdbcexception, use general query instead of the queryForObject, or use try/catch to convert the Jdbc related exception, else Spring itself will handle these internally using ExceptionTranslater, ExceptionHandler etc.
To handle optional case in controllers, just throw an exception there, for example PostController.java#L63
And handle it in the PostExceptionHandler.
Editing based on comment about stack trace
For your error please check - Jdbctemplate query for string: EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0
To solve problem associated with orderService.getOrderById(id) returning null you can return ResponseEntity.ResponseEntity gives you more flexibility in terms of status code and header. If you can change your code to return ResponseEntitythen you can do something like
#GetMapping(path = "{id}")
public ResponseEntity<?> getOrderById(#PathVariable("id") int id) {
return orderService
.getOrderById(id)
.map(order -> new ResponseEntity<>(order.getId(), HttpStatus.OK))
.orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
You can even write generic Exception handler using #ControllerAdvice and throw OrderNotFoundException as .orElse(throw new OrderNotFoundException);. Check more information here.
I'm trying to make a controller test. And I found some difficulties when testing applicationContext.getBeansOfType in the controller.
So here are the snippets of my controller code looks like
#RestController
#RequestMapping(value = "/api/bridge")
public class BridgeController {
#RequestMapping(
value = "/visitor",
method = RequestMethod.POST
)
public Mono<Response<Boolean>> visitorEvent(
#RequestBody VisitorEventWebRequest webRequest) {
return Mono
.fromCallable(() -> constructVisitorEventCommandRequest(webRequest))
.flatMap(register ->
commandExecutor.execute(getVisitorFactory(webRequest).getClass(),register)
.subscribeOn(SchedulerHelper);
}
}
with getVisitorFactory(webRequest) is a method to call the beans instance of service based on its eventType(this is a field in webRequest)
private VisitorEventAbstract getVisitorFactory(VisitorEventWebRequest webRequest) {
try {
VisitorEventAbstract command = this.applicationContext.getBeansOfType(VisitorEventAbstract.class)
.getOrDefault(webRequest.getEventType(), null);
if(command == null) {
throw new RuntimeException();
}
return command;
} catch (Exception e) {
log.error("Error getting visitor event abstract implementation for event type : {}",
webRequest.getEventType());
throw new RuntimeException();
}
}
And then, in my controller test, I have mock the applicationContext.getBeansOfType and it will return a map that I had declared in the #Before method. I'm using SpringExtension and WebFluxTest. The snippet is below
#ExtendWith(SpringExtension.class)
#WebFluxTest(controllers = BridgeController.class)
public class BridgeVisitorControllerTest {
#Autowired
private WebTestClient testClient;
#MockBean
private ApplicationContext applicationContext;
#MockBean
private RegisterEventCommandImpl registerEventCommandImpl;
private Map<String, VisitorEventAbstract> visitorEventAbstractContext;
#BeforeEach
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
this.visitorEventAbstractContext = new HashMap<>();
this.visitorEventAbstractContext.put("REGISTRATION", this.registerEventCommandImpl);
}
#Test
public void execute() {
/// some code
when(applicationContext.getBeansOfType(VisitorEventAbstract.class))
.thenReturn(this.visitorEventAbstractContext);
when(commandExecutor.execute(eq(VisitorEventAbstract.class), any()))
.thenAnswer(ans -> Mono.just(Boolean.TRUE));
/// testClient.post()
}
}
So, since I have initialized the map, and mock the applicationContext, I expect when the controllerTest run, in the getVisitorFactory(webRequest method),it will return the map like this
{
"key" : "REGISTRATION",
"value" : RegisterEventCommandImpl (bean instance that have been mocked)
}
but, in actual, the map returned always changed the key like this :
{
"key" : "{package name of the RegisterEventCommandImpl}",
"value" : RegisterEventCommandImpl (bean instance)
}
which make my test always failed because they can't find the bean with key "REGISTRATION". please help, what am I missing? and why the key is always change like that?
Thank you for your answer.
I am using custom validation in entity class, #Valid annotation on service class not in controller class and custom exception controller(#ControllerAdvice) in Spring Boot.
When I am using #Valid in controller the custom annotation is throwing MethodArgumentNotValidException and I am able to handle it.
The problem comes
when I am using #Valid in service class the custom annotation stopped thowing exception. I want to handle custom annotation in ConstraintViolationException. I am using custom annotation on object level not field level. Please help
I got the solution is is look like following:
#ExceptionHandler(ConstraintViolationException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ResponseBody
ValidationErrorReponse onConstraintValidationException(ConstraintViolationException e) {
ValidationErrorReponse error = new ValidationErrorReponse();
Map<String, List<String>> errorMap = new HashMap<>();
List<Violation> violations = new ArrayList<>();
for (ConstraintViolation<?> violation : e.getConstraintViolations()) {
if (!errorMap.containsKey(violation.getPropertyPath().toString())) {
List<String> errorMessages = new ArrayList<>();
if(!violation.getMessage().isEmpty()) {
errorMessages.add(violation.getMessage());
errorMap.put(violation.getPropertyPath().toString(), errorMessages);
}else {
ConstraintDescriptor<?> objEceptions = violation.getConstraintDescriptor();
errorMessages.add((String)objEceptions.getAttributes().get("errormessage"));
String errorField = (String)objEceptions.getAttributes().get("errorField");
errorMap.put(violation.getPropertyPath().toString().concat("."+errorField), errorMessages);
}
} else {
errorMap.get(violation.getPropertyPath().toString()).add(violation.getMessage());
}
}
for (Entry<String, List<String>> entry : errorMap.entrySet()) {
Violation violation = new Violation(entry.getKey(), entry.getValue());
violations.add(violation);
}
error.setViolations(violations);
return error;
}
}
Service gets data from DB in constructor and store it in HashMap and then returns data from HashMap. Please take a look:
#RestController
#RequestMapping("/scheduler/api")
#Transactional(readOnly = true, transactionManager = "cnmdbTm")
public class RestApiController {
private final Set<String> cache;
#Autowired
public RestApiController(CNMDBFqdnRepository cnmdbRepository, CNMTSFqdnRepository cnmtsRepository) {
cache = new HashSet<>();
cache.addAll(getAllFqdn(cnmdbRepository.findAllFqdn()));
cache.addAll(getAllFqdn(cnmtsRepository.findAllFqdn()));
}
#RequestMapping(value = "/fqdn", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public List<SchedulerRestDto> checkFqdn(#RequestBody List<SchedulerRestDto> queryList) throws ExecutionException {
for (SchedulerRestDto item : queryList) {
item.setFound(1);
if (!cache.contains(item.getFqdn())) {
item.setFound(0);
}
}
return queryList;
}
private Set<String> getAllFqdn(List<String> fqdnList) {
Set<String> result = new HashSet<>();
for (String fqdn : fqdnList) {
result.add(fqdn);
}
return result;
}
}
But I always get a result in about 2sec. I thought it's a bit slowly for 35K string which I got from DB.
I tried to find out where problem is. I store serialized HashMap to file and modified constructor to:
#Autowired
public RestApiController(CNMDBFqdnRepository cnmdbRepository, CNMTSFqdnRepository cnmtsRepository) {
try (final InputStream fis = getResourceAsStream("cache-hashset.ser");
final ObjectInputStream ois = new ObjectInputStream(fis)) {
cache = (Set<String>) ois.readObject();
}
}
after that service returned result less than 100 ms.
I think it's related with DB but I don't know exactly how I can fix it.
Any ideas?
After several hours of experiments I realized that the main cause is annotation #Transactional on the class.
When I moved this annotation on a method, service returned response quicker. In my final decision I moved this annotation to a another class. New constructor is
#Autowired
public RestApiController(FqdnService fqdnService, SqsService sqsService) {
Objects.requireNonNull(fqdnService);
cache = fqdnService.getCache();
}
Code is cleaner and there aren't any performance issues.
I have to cache the result of the following public method :
#Cacheable(value = "tasks", key = "#user.username")
public Set<MyPojo> retrieveCurrentUserTailingTasks(UserInformation user) {
Set<MyPojo> resultSet;
try {
nodeInformationList = taskService.getTaskList(user);
} catch (Exception e) {
throw new ApiException("Error while retrieving tailing tasks", e);
}
return resultSet;
}
I also configured Caching here :
#Configuration
#EnableCaching(mode = AdviceMode.PROXY)
public class CacheConfig {
#Bean
public CacheManager cacheManager() {
final SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("tasks"),new ConcurrentMapCache("templates")));
return cacheManager;
}
#Bean
public CacheResolver cacheResolver() {
final SimpleCacheResolver cacheResolver = new SimpleCacheResolver(cacheManager());
return cacheResolver;
}
}
I assert the following :
Cache is initialized and does exist within Spring Context
I used jvisualvm to track ConcurrentMapCache (2 instances), they are
there in the heap but empty
Method returns same values per user.username
I tried the same configuration using spring-boot based project and
it worked
The method is public and is inside a Spring Controller
The annotation #CacheConfig(cacheNames = "tasks") added on top of my
controller
Spring version 4.1.3.RELEASE
Jdk 1.6
Update 001 :
#RequestMapping(value = "/{kinematicId}/status/{status}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public DocumentNodeWrapper getDocumentsByKinematicByStatus(#PathVariable String kinematicId, #PathVariable String status, HttpServletRequest request) {
UserInformation user = getUserInformation(request);
Set<ParapheurNodeInformation> nodeInformationList = retrieveCurrentUserTailingTasks(user);
final List<DocumentNodeVO> documentsList = getDocumentsByKinematic(kinematicId, user, nodeInformationList);
List<DocumentNodeVO> onlyWithGivenStatus = filterByStatus(documentsList);
return new DocumentNodeWrapper("filesModel", onlyWithGivenStatus, user, currentkinematic);
}
Thanks
Is the calling method getDocumentsByKinematicByStatus() in the same bean as the cacheable method ? If true, then this is a normal behavior because you're not calling the cacheable method via proxy but directly.