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:
- Load the Person instance with
id=1
- Populate the properties of the loaded person entity with the XML payload using
Jaxb2RootElementHttpMessageConverter
orMappingJackson2XmlHttpMessageConverter
- Hand it to the controller's action handler as its
person
argument
Actual behavior:
- The Person instance with
id=1
is loaded - The instance's properties are not updated to match the XML in the request payload
- 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
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
- wrong mapping in controller
- to update entity you need to get it in persisted (managed) state first, then copy desired state on it.
- 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.
0 comments:
Post a Comment