Wednesday, January 4, 2017

How to write a RestController to update a JPA entity from an XML request, the Spring Data JPA way?

Leave a Comment

I have a database with one table named person:

 id | first_name | last_name | date_of_birth  ----|------------|-----------|---------------  1  | Tin        | Tin       | 2000-10-10     

There's a JPA entity named Person that maps to this table:

@Entity @XmlRootElement(name = "person") @XmlAccessorType(NONE) public class Person {      @Id     @GeneratedValue     private Long id;      @XmlAttribute(name = "id")     private Long externalId;      @XmlAttribute(name = "first-name")     private String firstName;      @XmlAttribute(name = "last-name")     private String lastName;      @XmlAttribute(name = "dob")     private String dateOfBirth;      // setters and getters } 

The entity is also annotated with JAXB annotations to allow XML payload in HTTP requests to be mapped to instances of the entity.

I want to implement an endpoint for retrieving and updating an entity with a given id.

According to this answer to a similar question, all I need to do is to implement the handler method as follows:

@RestController @RequestMapping(         path = "/persons",         consumes = APPLICATION_XML_VALUE,         produces = APPLICATION_XML_VALUE ) public class PersonController {      private final PersonRepository personRepository;      @Autowired     public PersonController(final PersonRepository personRepository) {         this.personRepository = personRepository;     }      @PutMapping(value = "/{person}")     public Person savePerson(@ModelAttribute Person person) {         return personRepository.save(person);     }  } 

However this is not working as expected as can be verified by the following failing test case:

@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = RANDOM_PORT) public class PersonControllerTest {      @Autowired     private TestRestTemplate restTemplate;      private HttpHeaders headers;      @Before     public void before() {         headers = new HttpHeaders();         headers.setContentType(APPLICATION_XML);     }      // Test fails     @Test     @DirtiesContext     public void testSavePerson() {         final HttpEntity<Object> request = new HttpEntity<>("<person first-name=\"Tin Tin\" last-name=\"Herge\" dob=\"1907-05-22\"></person>", headers);          final ResponseEntity<Person> response = restTemplate.exchange("/persons/1", PUT, request, Person.class, "1");         assertThat(response.getStatusCode(), equalTo(OK));          final Person body = response.getBody();         assertThat(body.getFirstName(), equalTo("Tin Tin")); // Fails         assertThat(body.getLastName(), equalTo("Herge"));         assertThat(body.getDateOfBirth(), equalTo("1907-05-22"));     }  } 

The first assertion fails with:

java.lang.AssertionError:  Expected: "Tin Tin"      but: was "Tin" Expected :Tin Tin Actual   :Tin 

In other words:

  • No server-side exceptions occur (status code is 200)
  • Spring successfully loads the Person instance with id=1
  • But its properties do not get updated

Any ideas what am I missing here?


Note 1

The solution provided here is not working.

Note 2

Full working code that demonstrates the problem is provided here.

More Details

Expected behavior:

  1. Load the Person instance with id=1
  2. Populate the properties of the loaded person entity with the XML payload using Jaxb2RootElementHttpMessageConverter or MappingJackson2XmlHttpMessageConverter
  3. Hand it to the controller's action handler as its person argument

Actual behavior:

  1. The Person instance with id=1 is loaded
  2. The instance's properties are not updated to match the XML in the request payload
  3. Properties of the person instance handed to the controller's action handler method are not updated

5 Answers

Answers 1

this '@PutMapping(value = "/{person}")' brings some magic, because {person} in your case is just '1', but it happens to load it from database and put to ModelAttribute in controller. Whatever you change in test ( it can be even empty) spring will load person from database ( effectively ignoring your input ), you can stop with debugger at the very first line of controller to verify it.

You can work with it this way:

@PutMapping(value = "/{id}") public Person savePerson(@RequestBody Person person, @PathVariable("id") Long id ) {     Person found = personRepository.findOne(id);      //merge 'found' from database with send person, or just send it with id     //Person merged..     return personRepository.save(merged);    } 

Answers 2

The problem is from the @ModelAttribute annotation which is web view annotation. Change to @RequestBody annotation for processing the body of the request in a restful controller.

Changing to @RequestBody will process xml string through converters using RequestResponseBodyMethodProcessor & RequestMappingHandlerAdapter

More here at http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-requestbody

You may also need to create composed annotation to serve other media types(json and form encoded) and stick it on top of controller methods.

This includes adding the consumes & produces annotation to controller methods based on request and response payload.

Composed annotations for the rest are provided here

https://github.com/sbrannen/spring-composed/tree/master/src/main/java/org/springframework/composed/web/rest

There are other ways to have the jaxb annotation work for json payload too. Jackson does support this through jackson-dataformat-xml dependency module.

You can look into spring data rest way too if you like to use the spring data jpa which has built in support for exposing the entities through rest without the need of controllers.

Answers 3

The issue is that when you call personRepository.save(person) your person entity does not have the primary key field(id) and so the database ends up having two records with the new records primary key being generated by the db. The fix will be to create a setter for your id field and use it to set the entity's id before saving it:

@PutMapping(value = "/{id}") public Person savePerson(@RequestBody Person person, @PathVariable("id") Long id) { person.setId(id); return personRepository.save(person); }

Also, like has been suggested by @freakman you should use @RequestBody to capture the raw json/xml and transform it to a domain model. Also, if you don't want to create a setter for your primary key field, another option may be to support an update operation based on any other unique field (like externalId) and call that instead.

Answers 4

  1. wrong mapping in controller
  2. to update entity you need to get it in persisted (managed) state first, then copy desired state on it.
  3. consider introducing DTO for your bussiness objects, as, later, responding with persisted state entities could cause troubles (e.g. undesired lazy collections fetching or entities relations serialization to XML, JSON could cause stackoverflow due to infinite method calls)

Below is simple case of fixing your test:

@PutMapping(value = "/{id}") public Person savePerson(@PathVariable Long id, @RequestBody Person person) {     Person persisted = personRepository.findOne(id);     if (persisted != null) {         persisted.setFirstName(person.getFirstName());         persisted.setLastName(person.getLastName());         persisted.setDateOfBirth(person.getDateOfBirth());         return persisted;     } else {         return personRepository.save(person);     } } 

Update

@PutMapping(value = "/{person}") public Person savePerson(@ModelAttribute Person person, @RequestBody Person req) {     person.setFirstName(req.getFirstName());     person.setLastName(req.getLastName());     person.setDateOfBirth(req.getDateOfBirth());     return person; } 

Answers 5

For updating any entity the load and save must be in same Transaction,else it will create new one on save() call,or will throw duplicate primary key constraint violation Exception.

To update any we need to put entity ,load()/find() and save() in same transaction, or write JPQL UPDATE query in @Repository class,and annotate that method with @Modifying .

@Modifying annotation will not fire additional select query to load entity object to update it,rather presumes that there must be a record in DB with input pk,which needs to update.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment