Customize update entity on Spring Data repository - spring

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);
}
}

Related

Is it possible to invoke mocked object's method in constructor?

I have, for example, these classes with Spring Boot. I try to do a REST API without a database and wieh files as data. The data files are like this:
{
"persons": [
{ "firstName":"John", "lastName":"Boyd", "address":"1509 Culver St", "city":"Culver", "zip":"97451", "phone":"841-874-6512", "email":"jaboyd#email.com" },
{ "firstName":"Jacob", "lastName":"Boyd", "address":"1509 Culver St", "city":"Culver", "zip":"97451", "phone":"841-874-6513", "email":"drk#email.com" }
] }
#Repository
public class PersonRepository {
private List<Person> persons;
private DataLoaderService loaderService;
#Autowired
public PersonRepository(DataLoaderService loaderService){
persons= loaderService.convertJsonToPojo("Persons",Person.class);
}
public List<Person> getAll(){
return persons;
}
}
#Service
public class DataLoaderService{
private JsonFileService jsonFileService;
private ObjectMapper mapper
#Autowired
public DataLoaderService(JsonFileService jsonFileService,ObjectMapper mapper){
this.JsonFileService =jsonFileService;
this.mapper=mapper;
}
public <T> List<T> convertJsonToPojo (String nodeName,Class <T>
classOfT){
}
}
So, I have a file. How can I read to transform to a list of Pojo?
When I want to mock the test method getAll(), my list size is 0. The mock doesn't give me values because I think the problem is that I initialized the value in the constructor. Here is my test:
#ExtendWith(MockitoExtension.class)
public class PersonRepositoryTest {
PersonRepository repository;
#Mock
private DataLoaderService loaderService;
#BeforeEach
public void setUp() {
repository = new PersonRepository(loaderService);
}
#Test
public void getAllPersonnesInConstructor() {
List<Person> mockedList = Arrays.asList(
new Person("Paul","Moes","1", "7777", "adresse tour", "Chicago", "pauln#gmail.com"),
new Person("Eleson","Moc","2", "77777", "ddkdkd", "New York", "eleson#gmail.com")
);
doReturn(mockedList).when(loaderService).convertJsonToPojo("persons",Person.class);
List<Person> persons = repository.getAll();
assertEquals(2,persons.size(),"Expected list size is 2");
assertEquals(persons,mockedList);
}
If i use #Spy, I have an error.
When I use method getAll() without initializing the variable persons in the constructor but in the method getAll, it is OK, like this:
public List<Person> getAll(){
this.persons=this.dataLoaderService.convertJsonToPojo("persons", Person.class);
log.debug("persons getALL repository" + persons);
return this.persons;
}
What can I do to test it?
Test a method which initializes a value in the constructor.

Spring TransactionManager behavior with Spring Data and JpaRepository

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);
}

DeleteById method returns null

I have tried to delete a row from database using Spring data deleteById method but it returns null.
ProductServiceImpl
public void removeOne(Long id) {
Product product = findById(id);
productRepository.deleteById(product);
ProductRepository
public interface ProductRepository extends CrudRepository<Product, Long> {
void deleteById(Product product);
Controller
#RequestMapping(value="/remove", method=RequestMethod.POST)
public String remove(#ModelAttribute("id") String id, Model model) {
productService.removeOne(Long.parseLong(id.substring(10)));
List<Product> productList = productService.findAll();
model.addAttribute("productList", productList);
System.out.println("deleted successfully !!!!");
return "redirect:/product/productList";
}
Why you write it complex. Some code not necessary .First, you extends CrudRepository,it mean you don't need create custom method void deleteById(Product product); because crud contain method deleteById.
Second, Controller why you using : #RequestMapping(value="/remove", method=RequestMethod.POST) . I think it must : #DeleteMapping("/remove") . And in controller only call.
#Autowired
private ProductRepository productRepository;
#DeleteMapping("/remove/{id}")
public String remove(#PathVariable String id) {
productRepository.deleteById(id);
return "redirect:/product/productList";
}
#Autowired
private ProductRepository productRepository;
#RequestMapping(value="/remove", method=RequestMethod.POST)
public String remove(#RequestParam String id) {
productRepository.deleteById(id);
return "redirect:/product/productList";
}

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();
}

Spring Rest Url ID Validation in DB - Eg: universities/{universityId}/campuses/{campusId}/buildings

I wanted to know the best practice of how to validate the ID of the path of my Rest API.
For example:
When I do a GET to retrieve a Building, I need to validate first if the {universityId} and {campusId} are actually valid (Existing in the DB) before proceeding.
Right now I have implemented a custom RepositoryValidation that provides those functionalities by throwing a ResourceNotFoundException() and those methods are called in my service class for the GET,PUT,POST..etc
Is there a better way to do the validation? I have read about Interceptors or Filters but not sure if that's the best practice.
Custom Exception:
#ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException() {
super();
}
public ResourceNotFoundException(String message) {
super(message);
}
Repository Validation:
#Component
public class RepositoryValidation {
#Autowired
private UniversityRepository universityRepository;
#Autowired
private CampusRepository campusRepository;
#Autowired
private BuildingRepository buildingRepository;
public void checkIfUniversityExists(Long universityId){
if (!universityRepository.exists(universityId))
throw new ResourceNotFoundException("University with id: " + universityId + " not found");
}
public void checkIfCampusExists(Long campusId){
if (!campusRepository.exists(campusId))
throw new ResourceNotFoundException("Campus with id: " + campusId + " not found");
}
public void checkIfBuildingExists(Long buildingId){
if (!buildingRepository.exists(buildingId))
throw new ResourceNotFoundException("Building with id: " + buildingId + " not found");
}
}
Service:
#Service
public class BuildingService {
#Autowired
private BuildingRepository buildingRepository;
#Autowired
private RepositoryValidation repositoryValidation;
public Iterable<Building> list(Long campusId) {
return buildingRepository.findAllByCampusId(campusId);
}
#Transactional
public Building create(Building building) {
return buildingRepository.save(building);
}
public Building read(Long buildingId,Long campusId) {
repositoryValidation.checkIfCampusExists(campusId);
repositoryValidation.checkIfBuildingExists(buildingId);
return buildingRepository.findBuildingByIdAndCampusId(buildingId,campusId);
}
#Transactional
public Building update(Long buildingId,Building update) {
repositoryValidation.checkIfBuildingExists(buildingId);
Building building = buildingRepository.findOne(buildingId);
building.setBuildingName(update.getBuildingName());
return buildingRepository.save(building);
}
#Transactional
public void delete(Long buildingId,Long campusId) {
repositoryValidation.checkIfCampusExists(campusId);
repositoryValidation.checkIfBuildingExists(buildingId);
buildingRepository.deleteBuildingByIdAndCampusId(buildingId, campusId);
}
You should look into Springs' Validation-Beanvalidation.
With this, you can use #Valid to do simple validations on properties, for example:
#NotNull
#Size(max=64)
private String name;
You can also add the #Valid to inputs in a REST endpoint:
#RequestMapping("/foo", method=RequestMethod.POST)
public void processFoo(#Valid Foo foo) { /* ... */ }
For your needs, you could consider creating a custom #Constraint.
You would first create the constraint annotation:
#Target({ElementType.METHOD, ElementType.FIELD})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy=MyConstraintValidator.class)
public #interface MyConstraint {
}
And then the constraint validator:
import javax.validation.ConstraintValidator;
public class MyConstraintValidator implements ConstraintValidator {
#Autowired;
private Foo aDependency;
...
}
Notice you can inject other Spring beans into the ConstraintValidator as well.
Once implemented, this could easily be re-used and looks very concise.

Resources