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.
Related
My question is : what repository will return when object not found in
junit tests.
I have test like this :
#RunWith(MockitoJUnitRunner.class)
#SpringBootTest
public class CouponServiceTestSuite {
private final static String HOME_TEAM = "home team";
private final static String AWAY_TEAM = "away team";
#Autowired
private CouponService couponService;
#MockBean
private CouponRepository couponRepository;
#MockBean
private MatchRepository matchRepository;
#Test
public void shouldThrowException() {
//Given
//When
//Then
Assertions.assertThrows(BetWinnerException.class, () -> couponService.getCoupon(-6L));
I want to mock this like :
Mockito.when(couponRepository.findById(ArgumentMatchers.anyLong()).thenReturn(null);
My service class :
#Slf4j
#RequiredArgsConstructor
#Service
public class CouponService {
private final CouponRepository couponRepository;
private final MatchRepository matchRepository;
private final CouponMapper couponMapper;
public List<CouponDto> getCoupons() {
log.debug("Getting all coupons");
List<Coupon> couponList = couponRepository.findAll();
List<CouponDto> couponDtoList = couponMapper.mapToCouponDtoList(couponList);
log.debug("Return all coupons: {}", couponDtoList);
return couponDtoList;
}
public CouponDto getCoupon(Long couponId) {
log.debug("Getting coupon by id: {}", couponId);
Coupon coupon = couponRepository.findById(couponId).orElseThrow(()
-> new BetWinnerException(BetWinnerException.ERR_COUPON_NOT_FOUND_EXCEPTION));
CouponDto couponDto = couponMapper.mapToCouponDto(coupon);
log.debug("Return coupon: {}", couponDto);
return couponDto;
}
public CouponDto createCoupon() {
log.debug("Creating new coupon");
Coupon coupon = couponRepository.save(new Coupon());
CouponDto couponDto = couponMapper.mapToCouponDto(coupon);
log.debug("Return created coupon: {}", couponDto);
return couponDto;
}
public CouponDto addMatch(Long couponId, Long matchId) {
log.debug("Add match to the coupon: {}{}", matchId, couponId);
Coupon coupon = couponRepository.findById(couponId).orElseThrow(()
-> new BetWinnerException(BetWinnerException.ERR_COUPON_NOT_FOUND_EXCEPTION));
Match match = matchRepository.findById(matchId).orElseThrow(()
-> new BetWinnerException(BetWinnerException.ERR_MATCH_NOT_FOUND_EXCEPTION));
coupon.getMatchList().add(match);
Coupon updatedCoupon = couponRepository.save(coupon);
CouponDto couponDto = couponMapper.mapToCouponDto(updatedCoupon);
log.debug("Return coupon with added match: {}", couponDto);
return couponDto;
}
public boolean deleteCoupon(Long couponId) {
log.debug("Deleting coupon id: {}", couponId);
couponRepository.deleteById(couponId);
if (couponRepository.existsById(couponId)) {
log.debug("Coupon not deleted id: {}", couponId);
return false;
} else {
log.debug("Coupon deleted id: {}", couponId);
return true;
}
}
}
I thought that it returns null but when i do like this it returns NullPointerException. My service returns BetWinnerException when object is not found.
So what it will return ? How should i create this test ?
Test like this works properly but i dont want to use id = -6. I just want to mock it somehow.
You are mocking couponRepository but using couponService and as there is no code shown how that is initialised in your test, it is hard to tell, where the problem is.
Now that you updated your question, the answer is quite obvious:
in the service the code expects couponRepository.findById() to return an Optional so that it can throw the exception if that is empty
mocked beans are 'normal' Mockito mocks that try to return a useful result; for collection this is an empty collection, for objects this is null - usually
TIL that Mockito 2 will support what you expect: https://www.baeldung.com/mockito-2-java-8#return-default-values-for-optional-and-stream
this article also shows how to make Mockito 1 return the empty Optional
you tried to make it return null but you actually need Optional.empty()
Or did I misunderstand how you actually test it and what your problem is?
Do you get a NullPointerException in the service as I understood?
Or do you already use Mockito 2 and have another issue?
Please post your entire test class
Add #InjectMocks annotation above the #Autowired in the service you declared in your test and see if it solves the problem, however I don't think it will work considering you have your Repo declared as FINAL in your Service. If it`s not FINAL and is #Autowired, the InjectMocks would work fine for your mock. But if you really need this as FINAL, try this:
https://www.baeldung.com/mockito-final
I am attempting to use the DataLoader feature within the graphql-java-kickstart library:
https://github.com/graphql-java-kickstart
My application is a Spring Boot application using 2.3.0.RELEASE. And I using version 7.0.1 of the graphql-spring-boot-starter library.
The library is pretty easy to use and it works when I don't use the data loader. However, I am plagued by the N+1 SQL problem and as a result need to use the data loader to help alleviate this issue. When I execute a request, I end up getting this:
Can't resolve value (/findAccountById[0]/customers) : type mismatch error, expected type LIST got class com.daluga.api.account.domain.Customer
I am sure I am missing something in the configuration but really don't know what that is.
Here is my graphql schema:
type Account {
id: ID!
accountNumber: String!
customers: [Customer]
}
type Customer {
id: ID!
fullName: String
}
I have created a CustomGraphQLContextBuilder:
#Component
public class CustomGraphQLContextBuilder implements GraphQLServletContextBuilder {
private final CustomerRepository customerRepository;
public CustomGraphQLContextBuilder(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
#Override
public GraphQLContext build(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
return DefaultGraphQLServletContext.createServletContext(buildDataLoaderRegistry(), null).with(httpServletRequest).with(httpServletResponse).build();
}
#Override
public GraphQLContext build(Session session, HandshakeRequest handshakeRequest) {
return DefaultGraphQLWebSocketContext.createWebSocketContext(buildDataLoaderRegistry(), null).with(session).with(handshakeRequest).build();
}
#Override
public GraphQLContext build() {
return new DefaultGraphQLContext(buildDataLoaderRegistry(), null);
}
private DataLoaderRegistry buildDataLoaderRegistry() {
DataLoaderRegistry dataLoaderRegistry = new DataLoaderRegistry();
dataLoaderRegistry.register("customerDataLoader",
new DataLoader<Long, Customer>(accountIds ->
CompletableFuture.supplyAsync(() ->
customerRepository.findCustomersByAccountIds(accountIds), new SyncTaskExecutor())));
return dataLoaderRegistry;
}
}
I also have create an AccountResolver:
public CompletableFuture<List<Customer>> customers(Account account, DataFetchingEnvironment dfe) {
final DataLoader<Long, List<Customer>> dataloader = ((GraphQLContext) dfe.getContext())
.getDataLoaderRegistry().get()
.getDataLoader("customerDataLoader");
return dataloader.load(account.getId());
}
And here is the Customer Repository:
public List<Customer> findCustomersByAccountIds(List<Long> accountIds) {
Instant begin = Instant.now();
MapSqlParameterSource namedParameters = new MapSqlParameterSource();
String inClause = getInClauseParamFromList(accountIds, namedParameters);
String sql = StringUtils.replace(SQL_FIND_CUSTOMERS_BY_ACCOUNT_IDS,"__ACCOUNT_IDS__", inClause);
List<Customer> customers = jdbcTemplate.query(sql, namedParameters, new CustomerRowMapper());
Instant end = Instant.now();
LOGGER.info("Total Time in Millis to Execute findCustomersByAccountIds: " + Duration.between(begin, end).toMillis());
return customers;
}
I can put a break point in the Customer Repository and see the SQL execute and it returns a List of Customer objects. You can also see that the schema wants an array of customers. If I remove the code above and put in the resolver to get the customers one by one....it works....but is really slow.
What am I missing in the configuration that would cause this?
Can't resolve value (/findAccountById[0]/customers) : type mismatch error, expected type LIST got class com.daluga.api.account.domain.Customer
Thanks for your help!
Dan
Thanks, #Bms bharadwaj! The issue was on my side in understanding how the data is returned in the dataloader. I ended up using a MappedBatchLoader to bring the data in a map. The key in the map being the accountId.
private DataLoader<Long, List<Customer>> getCustomerDataLoader() {
MappedBatchLoader<Long, List<Customer>> customerMappedBatchLoader = accountIds -> CompletableFuture.supplyAsync(() -> {
List<Customer> customers = customerRepository.findCustomersByAccountId(accountIds);
Map<Long, List<Customer>> groupByAccountId = customers.stream().collect(Collectors.groupingBy(cust -> cust.getAccountId()));
return groupByAaccountId;
});
// }, new SyncTaskExecutor());
return DataLoader.newMappedDataLoader(customerMappedBatchLoader);
}
This seems to have done the trick because before I was issuing hundreds of SQL statement and now down to 2 (one for the driver SQL...accounts and one for the customers).
In the CustomGraphQLContextBuilder,
I think you should have registered the DataLoader as :
...
dataLoaderRegistry.register("customerDataLoader",
new DataLoader<Long, List<Customer>>(accountIds ->
...
because, you are expecting a list of Customers for one account Id.
That should work I guess.
My Spring REST program, a slight extension of a Stephen Zerhusen demo using Json Web Tokens (JWT), works OK -- as far as it goes. I added an Option object, and I can GET, PUT and POST using just an Option class (#Entity) and an OptionRepository interface (extends JpaRepository)
I'm now trying, but failing, to restrict the returned data to just what the logged-in user has rights to. As an example, suppose that my logged in user only has rights to Option values 1, 3, and 5.
If I have a service call like GET /option I should not return Option values 2 or 4.
If I have a service call like GET /option/2 I should get back a HTTP 404 result.
I understand that once the user has logged in I can get their user information through a Principal object reference. Such a solution was offered in this previous stackoverflow question, and other pages also offer similar solutions.
My immediate problem is to find where I can affect the GET and PUT behavior of /option. Here is all that I added to an existing, working demo. First the entity defining class.
#Entity
#Table(name="choice")
public class Option implements Serializable {
#Id
#Column(name="id")
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id = Utilities.INVALID_ID;
#Column(name="value", length=50, nullable=false)
private String value;
#Column(name="name", length=100, nullable=false)
private String name;
public Long getId() { return this.id; }
public void setId(Long id) { this.id = id; }
public String getValue() { return this.value; }
public void setValue(String value) { this.value = value; }
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
}
Now the JpaRepository interface extension:
#RepositoryRestResource(collectionResourceRel="option", path="option")
public interface OptionRepository extends JpaRepository<Option, Long> {
}
I merely added those two files to the program and GET, PUT and POST work. BTW, it turns out that if I comment out the #RepositoryRestResource statement the call to /option/1 returns HTTP 404. Some documentation suggests it isn't needed, but I guess it really is.
Now to filter the output. Let's pretend to filter by making the server always return Option (id = 5). I do this by:
#RepositoryRestResource(collectionResourceRel="option", path="option")
public interface OptionRepository extends JpaRepository<Option, Long> {
#RequestMapping(path = "/option/{id}", method = RequestMethod.GET)
#Query("from Option o where o.id = 5")
public Iterable<Option> getById(#PathVariable("id") Long id);
}
When I run this server and do GET /option/1 I get back ... Option 1, not Option 5. The #Query isn't used.
What is the magic needed to affect the GET, PUT, etc?
Thanks,
Jerome.
You can use Resource Processor to manipulate returned resources:
#Component
public class OptionResourceProcessor implements ResourceProcessor<Resource<Option>> {
#Override
public Resource<Option> process(Resource<Option> resource) {
Option option = resource.getContent();
if (/* Logged User is not allowed to get this Option */ ) {
throw new MyCustomException(...);
} else {
return resource;
}
}
}
Then you can create custom Exception handler, for example:
#ControllerAdvice
public class ExceptionsHandler {
#ExceptionHandler(MyCustomException.class)
public ResponseEntity<?> handleMyCustomException(MyCustomException e) {
return new ResponseEntity<>(new MyCustomMessage(e), HttpStatus.FORBIDDEN);
}
}
To add some logic to PUT/POST/DELETE request you can use a custom Event Handler, for example:
#RepositoryEventHandler(Option.class)
public class OptionEventHandler {
#HandleBeforeSave
public void handleBeforeSave(Option option) {
if (/* Logged User is not allowed to save this Option */ ) {
throw new MyCustomException(...);
}
}
}
You can find more SDR usage examples in my sample project...
I'm trying to implement a partial update of the Manager entity based in the following:
Entity
public class Manager {
private int id;
private String firstname;
private String lastname;
private String username;
private String password;
// getters and setters omitted
}
SaveManager method in Controller
#RequestMapping(value = "/save", method = RequestMethod.PATCH)
public #ResponseBody void saveManager(#RequestBody Manager manager){
managerService.saveManager(manager);
}
Save object manager in Dao impl.
#Override
public void saveManager(Manager manager) {
sessionFactory.getCurrentSession().saveOrUpdate(manager);
}
When I save the object the username and password has changed correctly but the others values are empty.
So what I need to do is update the username and password and keep all the remaining data.
If you are truly using a PATCH, then you should use RequestMethod.PATCH, not RequestMethod.POST.
Your patch mapping should contain the id with which you can retrieve the Manager object to be patched. Also, it should only include the fields with which you want to change. In your example you are sending the entire entity, so you can't discern the fields that are actually changing (does empty mean leave this field alone or actually change its value to empty).
Perhaps an implementation as such is what you're after?
#RequestMapping(value = "/manager/{id}", method = RequestMethod.PATCH)
public #ResponseBody void saveManager(#PathVariable Long id, #RequestBody Map<Object, Object> fields) {
Manager manager = someServiceToLoadManager(id);
// Map key is field name, v is value
fields.forEach((k, v) -> {
// use reflection to get field k on manager and set it to value v
Field field = ReflectionUtils.findField(Manager.class, k);
field.setAccessible(true);
ReflectionUtils.setField(field, manager, v);
});
managerService.saveManager(manager);
}
Update
I want to provide an update to this post as there is now a project that simplifies the patching process.
The artifact is
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.13</version>
</dependency>
The implementation to patch the Manager object in the OP would look like this:
Controller
#Operation(summary = "Patch a Manager")
#PatchMapping("/{managerId}")
public Task patchManager(#PathVariable Long managerId, #RequestBody JsonPatch jsonPatch)
throws JsonPatchException, JsonProcessingException {
return managerService.patch(managerId, jsonPatch);
}
Service
public Manager patch(Long managerId, JsonPatch jsonPatch) throws JsonPatchException, JsonProcessingException {
Manager manager = managerRepository.findById(managerId).orElseThrow(EntityNotFoundException::new);
JsonNode patched = jsonPatch.apply(objectMapper.convertValue(manager, JsonNode.class));
return managerRepository.save(objectMapper.treeToValue(patched, Manager.class));
}
The patch request follows the specifications in RFC 6092, so this is a true PATCH implementation. Details can be found here
With this, you can patch your changes
1. Autowire `ObjectMapper` in controller;
2. #PatchMapping("/manager/{id}")
ResponseEntity<?> saveManager(#RequestBody Map<String, String> manager) {
Manager toBePatchedManager = objectMapper.convertValue(manager, Manager.class);
managerService.patch(toBePatchedManager);
}
3. Create new method `patch` in `ManagerService`
4. Autowire `NullAwareBeanUtilsBean` in `ManagerService`
5. public void patch(Manager toBePatched) {
Optional<Manager> optionalManager = managerRepository.findOne(toBePatched.getId());
if (optionalManager.isPresent()) {
Manager fromDb = optionalManager.get();
// bean utils will copy non null values from toBePatched to fromDb manager.
beanUtils.copyProperties(fromDb, toBePatched);
updateManager(fromDb);
}
}
You will have to extend BeanUtilsBean to implement copying of non null values behaviour.
public class NullAwareBeanUtilsBean extends BeanUtilsBean {
#Override
public void copyProperty(Object dest, String name, Object value)
throws IllegalAccessException, InvocationTargetException {
if (value == null)
return;
super.copyProperty(dest, name, value);
}
}
and finally, mark NullAwareBeanUtilsBean as #Component
or
register NullAwareBeanUtilsBean as bean
#Bean
public NullAwareBeanUtilsBean nullAwareBeanUtilsBean() {
return new NullAwareBeanUtilsBean();
}
First, you need to know if you are doing an insert or an update. Insert is straightforward. On update, use get() to retrieve the entity. Then update whatever fields. At the end of the transaction, Hibernate will flush the changes and commit.
You can write custom update query which updates only particular fields:
#Override
public void saveManager(Manager manager) {
Query query = sessionFactory.getCurrentSession().createQuery("update Manager set username = :username, password = :password where id = :id");
query.setParameter("username", manager.getUsername());
query.setParameter("password", manager.getPassword());
query.setParameter("id", manager.getId());
query.executeUpdate();
}
ObjectMapper.updateValue provides all you need to partially map your entity with values from dto.
As an addition, you can use either of two here: Map<String, Object> fields or String json, so your service method may look like this:
#Autowired
private ObjectMapper objectMapper;
#Override
#Transactional
public Foo save(long id, Map<String, Object> fields) throws JsonMappingException {
Foo foo = fooRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Foo not found for this id: " + id));
return objectMapper.updateValue(foo , fields);
}
As a second solution and addition to Lane Maxwell's answer you could use Reflection to map only properties that exist in a Map of values that was sent, so your service method may look like this:
#Override
#Transactional
public Foo save(long id, Map<String, Object> fields) {
Foo foo = fooRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Foo not found for this id: " + id));
fields.keySet()
.forEach(k -> {
Method method = ReflectionUtils.findMethod(LocationProduct.class, "set" + StringUtils.capitalize(k));
if (method != null) {
ReflectionUtils.invokeMethod(method, foo, fields.get(k));
}
});
return foo;
}
Second solution allows you to insert some additional business logic into mapping process, might be conversions or calculations ect.
Also unlike finding reflection field Field field = ReflectionUtils.findField(Foo.class, k); by name and than making it accessible, finding property's setter actually calls setter method that might contain additional logic to be executed and prevents from setting value to private properties.
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.