How to write RestController to update a JPA object from an XML request, in the manner of Spring Data JPA?

I have a database with one table named person:

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

There is a JPA object 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 the loading of XML into HTTP requests for mapping to object instances.

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

According to this answer to a similar question , all I have to do is 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 does not work as expected, which can be confirmed by the following unsuccessful 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 statement fails:

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

In other words:

  • There are no server-side exceptions (status code 200 )
  • Spring successfully loads Person instance using id=1
  • But its properties are not updated.

Any ideas what I'm missing here?


Note 1

The solution provided here does not work.

Note 2

Full working code demonstrating the problem is provided here .

More details

Expected Behavior:

  • Download an instance of Person using id=1
  • Fill the properties of the loaded face object with the XML payload using Jaxb2RootElementHttpMessageConverter or MappingJackson2XmlHttpMessageConverter
  • Pass it to the controller action handler as its Person argument

Actual behavior:

  • An instance of Person is loaded with id=1
  • Instance properties are not updated according to XML in the request payload
  • The properties of the face instance passed to the controller handler method are not updated
+5
source share
5 answers

this '@PutMapping (value = "/ {person}")' brings some magic, because {person} in your case is only 1, but it loads it from the database and puts it in the ModelAttribute in the controller. No matter what you change in the test (it may even be empty) spring loads a person from the database (actually ignoring your input), you can stop it using the debugger in the very first line of the controller to check it.

You can work with it as follows:

 @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); } 
+3
source

The problem is the @ModelAttribute annotation, which is a web view annotation. Change the annotation to @RequestBody to process the request body in a quiet controller.

Going to @RequestBody will process the xml string through the converters using RequestResponseBodyMethodProcessor and RequestMappingHandlerAdapter

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

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

This includes adding flow meters and annotating controller methods based on the request and response payload.

Here are the attached annotations for the rest

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

There are other ways to work with jaxb annotation for the json payload. Jackson supports this through the jackson-dataformat-xml dependency module.

You can also look in spring data storage mode if you want to use jpa spring data, which has built-in support for demonstrating objects through rest without the need for controllers.

0
source

The problem is that when calling personRepository.save(person) , your person does not have a primary key field (id), and therefore the database ends up having two records with a new key for new records generated by db. The fix will be to create a setter for your id field and use it to set the identifier of the object before saving it:

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

Also, as @freakman suggested, you should use @RequestBody to capture the original json / xml and convert it to a domain model. In addition, if you do not want to create a setter for your primary key field, another option may be to support the update operation based on any other unique field (for example, externalId) and the subsequent call.

0
source
  • incorrect display in the controller
  • To update an object, you first need to transfer it to the state with the saved (managed) one, and then copy the desired state to it.
  • consider introducing a DTO for your business objects, since later responding with stateful objects can cause problems (for example, unwanted lazy collections or binding entities to XML serialization, JSON can cause a stack overflow due to endless method calls)

Below is a simple example of checking 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; } 
0
source

To update any object, loading and saving must be in the same transaction, otherwise it will create a new one in the save () call or cause a duplicate violation of the primary key Exception constraints.

To update everything, we need to put the objects, load () / find () and save () in one transaction or write a JPQL UPDATE query in the @Repository class and annotate this method using @Modification.

@ Modification of the annotation will not trigger an additional selection request to load the object's object to update it; rather, it assumes that there must be an entry in the database with the pk input that needs to be updated.

0
source

Source: https://habr.com/ru/post/1261861/


All Articles