Spring TransactionManager behavior with Spring Data and JpaRepository - spring

I have a controller which does the following
A submit end point which save an entry in db and then call some external service asynchronously
Track the update of asynchronous call (this call updates an associated table) by watching the db and update the status of the entry created in step one
I was using the #Query Annotation to verify if step one entry exist in db and it was always returning empty. I tried changing it to the default spring method and it starts returning the inserted value.
I read about proxies, #Transactional and how non CRUD methods in a JPARepository are non transactional and tried few things like transaction propagation and self injection and even explicitly marking the repo method #Transactional. But none of them fixed the issue. Using spring data method solved it but I still don't understand what happened. Can someone help with an explanation of this behavior.
Basic code snippet is below
MyController
#RestController
public class MyController {
private final MyService myService;
private final MyRepository myRepository;
#Autowired
public MyController(MyService myService,
MyRepository myRepository) {
this.myService = myService;
this.myRepository = myRepository;
}
#PostMapping(value = "/submit")
public ResponseEntity<MyResponse> submit(#Valid #RequestBody MyRequest myRequest) {
return ResponseEntity
.accepted()
.body(MyResponse.success(myService.submit(myRequest), "SUBMITTED"));
}
/**
* This method is to update the status of the entry created by /submit endpoint
* if the asynchoronous process triggered by submit endpoint update an associated table
*/
#PostConstruct
private void trackUpdates() {
..
someObserver.subscribe(trackedAssociatedEntity -> {
myService.trackAndUpdateBasedOnAssociatedEntity(trackedAssociatedEntity);
});
}
}
MyService
#Service
#Transactional
public class MyService {
private final MyRepository myRepository;
#Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
submit(MyRequest myRequest) {
myRepository.save(myEntity);
//makes that asynchronous call
}
public void trackAndUpdateBasedOnAssociatedEntity(#NotNull MyAssociatedEntity myassociatedEntity) {
// This commented call always return empty but the uncommented code works as expected
// List<MyEntity> existingEntity =
// myRepository.findEntityByField1AndField2(myassociatedEntity.getField1(),myassociatedEntity.getField2());
List<MyEntity> existingEntities =
myRepository.findByField1AndField2(myassociatedEntity.getField1(),myassociatedEntity.getField2());
if(existingEntities.isEmpty()){
//create new
}else{
//update
}
}
}
}
}
MyRepository
#Repository
public interface MyRepository extends JpaRepository<MyEntity, Long> {
#Query("SELECT e FROM MyEntity e WHERE e.field1 = ':field1' and e.field2 = ':field2' ")
List<MyEntity> findEntityByField1AndField2(String field1, String field2);
List<MyEntity> findByField1AndField2(String field1, String field2);
}

I believe that '' are not needed. Please try the following:
#Repository
public interface MyRepository extends JpaRepository<MyEntity, Long> {
#Query("SELECT e FROM MyEntity e WHERE e.field1 = :field1 and e.field2 = :field2")
List<MyEntity> findEntityByField1AndField2(String field1, String field2);
List<MyEntity> findByField1AndField2(String field1, String field2);
}

Related

Mocked JPA repository bean is not returning assigned value from a mocked method call

I have a interface CalculatorService whose implementation contains business logic.
Interface:
public interface CalculatorService {
LoanWeb calculateSimpleLoan(LoanWeb loanWeb) throws Exception;
}
Implementation:
#Service
#AllArgsConstructor
public class CalculatorServiceImpl implements CalculatorService {
private final LoanRepository loanRepository; //this extends JpaRepository
private final PropertiesConfig propertiesConfig;
private final CalculatorUtility calculatorUtility;
private final LoanInfoRepository loanInfoRepository;
#Override
public LoanWeb calculateSimpleLoan(LoanWeb loanWeb) throws Exception {
//validation
if (loanWeb.getLoanTerm() == null || loanWeb.getLoanTerm() <= 0) throw new LeanPayException(ErrorCode.INVALID_INTEGER_ERROR.code, List.of("loanTerm"));
var payments = someCalculation();
Loan loan = loanRepository.findFirstByAmountAndRateAndPaymentsAndFrequency
(loanWeb.getLoanAmount(), loanWeb.getInterestRate(), payments, Frequency.MONTHLY); //loan is null???!!!
if (loan != null) return LoanWeb.builder() //this is false, I need it to be true!
.monthlyPayment(loan.getPmt())
.totalInterestPaid(loan.getLoanInfos().stream().mapToDouble(LoanInfo::getInterest).sum()).build();
}
}
JpaRepository:
#Repository
public interface LoanRepository extends JpaRepository<Loan, Long> {
Loan findFirstByAmountAndRateAndPaymentsAndFrequency(Double amount, Double rate, Integer payments, Frequency frequency);
}
Test class:
It looks like this:
#SpringBootTest(properties = {"spring.cloud.config.enabled: false", "logging.level.com.package.calculator: OFF"},
classes= {ObjectMapper.class, CalculatorServiceImpl.class, LoanRepository.class})
public class CalculatorServiceTest {
#Autowired private ObjectMapper objectMapper;
#Autowired
private CalculatorService calculatorService;
#MockBean(classes = LoanRepository.class) private LoanRepository loanRepository;
#MockBean private PropertiesConfig propertiesConfig;
#MockBean private CalculatorUtility calculatorUtility;
#MockBean private LoanInfoRepository loanInfoRepository;
#MockBean private TestService testService;
private Loan loan1; //this is not null, it is correctly loaded from a file.
#PostConstruct
public void init() throws IOException{
String jsonString =
IOUtils.toString(
Objects.requireNonNull(this.getClass().getResourceAsStream("/json/PreExistingLoan.json")), StandardCharsets.UTF_8);
this.loan1 = objectMapper.readValue(jsonString, Loan.class);
}
#Test
void calculateSimpleLoanWithPreExistingLoan() throws LeanPayException {
Mockito.doReturn(this.loan1).when(loanRepository).findFirstByAmountAndRateAndPaymentsAndFrequency( ArgumentMatchers.anyDouble(), ArgumentMatchers.anyDouble(),
ArgumentMatchers.anyInt(), ArgumentMatchers.any(Frequency.class));
LoanWeb result = this.calculatorService.calculateSimpleLoan(LoanWeb.builder().loanTerm(10).simpleLoanTerm(SimpleLoanTerm.MONTH).build());
Assertions.assertEquals(result.getTotalInterestPaid(), 10);
Assertions.assertEquals(result.getMonthlyPayment(), 107.0);
}
}
Application context is brought up, test runs, but mocked repo method call returns null in service. Where am I wrong? I tried million things, this is code from beginning, don't have any ideas.
ArgumentMatchers.anyDouble() matches any double (primitive) or non-null Double (boxed) value.
If you happen to pass a null value, it won't be matched.
You have a couple of options:
use ArgumentMatchers.any() which matches anything, including nulls
use ArgumentMatchers.eq(null) on the argument where the null is passed
modify your test to pass a non-null value

#PreAuthorize how do i add a parameter into the expression

Maybe somebody has an idea
I have an abstract controller providing me the endpoints i need.
In the #PreAuthorized i would execute the check if the user has the required roles.
Problem is that i have only one function and i want to check which endpoint is currently evaluated.
This is the code:
public abstract class CoreController<T> {
private final JpaRepository repository;
private final CoreService service;
public String endpoint;
private String view;
public CoreController(CoreService service, JpaRepository repository, String endpoint, String view) {
this.service=service;
this.repository = repository;
this.endpoint=endpoint;
this.view=view;
}
#PreAuthorize("#checkAccess.isAllowedToGet(#endpoint)")
#RequestMapping(method = RequestMethod.GET, value = "/get")
public ResponseEntity<CrudPage<Map<String, Object>>> get(CoreCriteria criteria) {
criteria.setView(view);
CrudPage<Map<String, Object>> data = service.getPage(criteria);
return ResponseEntity.ok(data);
}
The problem is the endpoint is always null when the function is called.
How should i change the expression to make this work.

Spring Data - PagingAndSortingRepository with custom query (HQL)?

Trying to mix PagingAndSortingRepository with custom queries, no luck..
Custom repo:
public interface SiteRepositoryCustom
{
public List<SitesDbRecord> getActiveSites();
}
Impl repo:
#Repository
public class SiteRepositoryImpl implements SiteRepositoryCustom
{
private static final Logger logger = ...
#PersistenceContext
private EntityManager em;
#Override
public List<SitesDbRecord> getActiveSites()
{
logger.info( "getActiveSites start" );
try
{
String hql = "select s from SitesDbRecord s where s.isActive = true";
return em.createQuery( hql ).setMaxResults( Integer.MAX_VALUE ).getResultList();
}
catch ( Exception e )
{
logger.error( "getActiveSites failed.", e );
return null;
}
}
}
The repo injected to the service:
public interface SiteRepository extends PagingAndSortingRepository<SitesDbRecord, Integer>, SiteRepositoryCustom {
public List<SitesDbRecord> getActiveSites( Pageable pageable );
public List<SitesDbRecord> getActiveSites();
}
If I just extend CrudRepository (without the Pageable method) then all is OK. Trying to extend PagingAndSortingRepository (with or without the Pageable method) then Spring fails to boot with
PropertyReferenceException: No property getActiveSites found for type SitesDbRecord!
What is the correct way to use PagingAndSortingRepository with custom queries? Probably got it wrong, but I assumed it's Spring responsibility to provide the handling of paging/sorting.
If SitesDbRecord has boolean property named active it should be:
public interface SiteRepository extends PagingAndSortingRepository<SitesDbRecord, Integer> {
public List<SitesDbRecord> findByActiveIsTrue( Pageable pageable );
public List<SitesDbRecord> findByActiveIsTrue();
}
There is no need to extend your Custom repository, just implement PagingAndSortingRepository

Customize update entity on Spring Data repository

I have XRepository interface (extends JpaRepository). On create or update X entity i need to call method of another repository (YRepository) in transaction (exactly: update some field and use new value in created/updated entity X).
To do that i created a service class with #Transactional methods and custom REST Controller. POST mapping on controller works OK and is acceptable for me, but have problem how to implement in more elegant way update (PUT/PATCH) existing entity in my service layer. It works too, but had to use BeanUtils.copyProperties(). Is a better, more conventional way to do that ?
public interface XRepository extends JpaRepository<X, Long> {
}
public interface YRepository extends JpaRepository<Y, Long> {
}
#BasePathAwareController
public class XRestControllerCustom {
#Autowired
private MyService myService;
#PostMapping("/x")
public #ResponseBody ResponseEntity<X> create(#RequestBody Resource<X> x) {
return new ResponseEntity<X>(myService.save(x.getContent()), HttpStatus.CREATED);
}
#PatchMapping("/x/{id}")
public #ResponseBody ResponseEntity<X> update(#PathVariable Long id, #RequestBody Resource<X> x) {
myService.update(id, x.getContent());
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
#Component
public class MyService {
#Autowired
private XRepository xRepository;
#Autowired
private YRepository yRepository;
#Transactional
public X save(X x) {
yRepository.update();
x.setField(yRepository.get());
return xRepository.save(x);
}
#Transactional
public X update(Long id, X partial) {
X x = xRepository.getOne(id);
BeanUtils.copyProperties(x, partial);
x.setId(id); // because copyProperties makes id null
yRepository.update();
x.setField(yRepository.get());
return xRepository.save(x);
}
}

Get all documents from an index using spring-data-elasticsearch

I am trying to connect to my external ElasticSearch server with Spring Boot.
If I do a curl from command line, I get expected results.
curl "http://ipAddr:9200/indexName/TYPE/_search?pretty=true"
But getting this error when I try to access it via Spring Boot.
<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Mon Sep 11 12:39:15 IST 2017</div><div>There was an unexpected error (type=Internal Server Error, status=500).</div><div>Could not write JSON: (was java.lang.NullPointerException); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: java.util.ArrayList[0]->org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl["facets"])</div></body></html>
Not sure why a NullPointerException and what is aggregartion.impl
Here is my Spring Application:
Controller:
#RestController
public class PojoController {
#Autowired
PojoService pojoService;
#RequestMapping(value = "/", method=RequestMethod.GET)
public #ResponseBody String index() {
return new String("Welcome:)");
}
#RequestMapping(value = "/all", method = RequestMethod.GET,
produces = { MediaType.APPLICATION_JSON_VALUE })
#ResponseBody List<POJO> findAll() {
try {
List<POJO> pojoObj = pojoService.findAll();
return pojoObj;
} catch (Exception exp) {
exp.printStackTrace();
return null;
}
}
}
Repository:
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
List<POJO> findAll();
}
Service:
#Service
public class POJOServiceImpl implements POJOService{
private POJORepository pojoRepository;
private ElasticsearchTemplate elasticsearchTemplate;
#Autowired
public void setPojoRepository(PojoRepository pojoRepository) {
this.pojoRepository = pojoRepository;
}
public POJO findOne(String id) {
return pojoRepository.findOne(id);
}
public List<POJO> findAll() {
return (List<POJO>) pojoRepository.findAll();
}
}
POJO class:
#Document(indexName = "INDEX", type = "TYPE")
public class POJO {
#Id
private Integer id;
private String name;
public POJO(){
// empty
}
public POJO(Integerid, String name) {
super();
this.id = id;
this.name = name;
}
// getters and setters
}
I should be able to query all the documents in the index. Later on, I will try and use filters etc.
Any help is appreciated. Thanks :)
It looks like Jackson has a problem with handling your POJO (probably related to this issue: DATAES-274) - the problematic part is casting in repository from Iterable collection to List.
Update
In case of repositories, spring-data-elasticsearch behaves a bit different than you would expect. Taking your example:
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
List<POJO> findAll();
}
and after calling in your rest controller:
List<POJO> pojoObj = pojoService.findAll();
in debugger you will see something like this:
You would expect that pojoObj list contains objects of POJO class.
And here comes the surprise - pojoObj ArrayList contains one object of AggregatedPageImpl type and its content field is the right list that contains your POJO objects.
This is the reason why you get:
Could not write JSON: ... java.util.ArrayList[0]->org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl[\"facets\"])
As I wrote before, Jackson cannot handle this while serializing POJO objects.
Solution 1
Let repositories return Iterable collection (by default).
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
}
Move the conversion part to the service but use some utility method (here with Guava) in order to have it like this:
import com.google.common.collect.Lists;
public List<POJO> findAll() {
return Lists.newArrayList(pojoRepository.findAll());
}
Solution 2
Use Page in repository (here simplified version without parameters):
#Repository
public interface PojoRepository extends ElasticsearchRepository<POJO, Integer> {
Page<TestDto> findAll();
}
If you still want to operate on list - get content from page in service:
public List<POJO> findAll() {
return testDtoRepository.findAll().getContent();
}

Resources