Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
like image 758
Behrang Avatar asked Dec 25 '16 07:12

Behrang


2 Answers

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);
   }
like image 54
hi_my_name_is Avatar answered Nov 03 '22 08:11

hi_my_name_is


  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;
}
like image 22
nike.laos Avatar answered Nov 03 '22 06:11

nike.laos