5

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
Community
  • 1
  • 1
βξhrαng
  • 41,698
  • 21
  • 103
  • 145
  • 4
    How does it fail? What does the response actually contain? Does it save the person in the database? What happens when you use the debugger and see what happens? You need to read error messages, and investigate. – JB Nizet Dec 25 '16 at 07:33
  • Somebody has voted to close this because it is _not programing_! What a joke. – βξhrαng Dec 25 '16 at 09:25
  • 1
    Somebody else asked you some questions. – Stephane Nicoll Dec 28 '16 at 08:52
  • And I have answered him (at the very bottom of the question). I have also provided the complete source code together with integration tests that demonstrate the issue. – βξhrαng Dec 28 '16 at 14:43
  • I have added even more information in the _More Details_ section. Let me know if you need more details. – βξhrαng Dec 28 '16 at 15:05

4 Answers4

3

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);
   }
freakman
  • 4,394
  • 1
  • 25
  • 47
  • _Whatever you change in test ( it can be even empty) spring will load person from database ( effectively ignoring your input )_. For URL Encoded Form data, the person is loaded and its properties are updated according to the request data. So the question becomes, how to do the "merge" elegantly? Suppose your entity has a dozen of attributes and associations. Merging the existing entity with the request body manually becomes very messy. Even worse if you have more than one entity. From what I can see Spring is missing this very crucial feature. – βξhrαng Dec 29 '16 at 07:08
  • 1
    yes, but spring doesnt know if you want to keep data from db, or it should just cherry-pick some of them and update. You can take whole 'person' from input and just save it ( you have an id set ), or decide which field you need to update using converter. Another thing: I would not recommend having domain object sent directly to controller - as you said - it may be complicated object. Define what you really can change through API, create model object + converter from model to domain. You can take a look at dozer - http://dozer.sourceforge.net/documentation/about.html – freakman Dec 29 '16 at 07:29
  • @Behrang _So the question becomes, how to do the "merge" elegantly?_ Try `BeanUtils.copyProperties(found, person);` https://commons.apache.org/proper/commons-beanutils/apidocs/org/apache/commons/beanutils/BeanUtils.html – A. Tim Dec 30 '16 at 18:12
  • +1, posted my answer before really figured out that this one is the same. Also +1 for DTO and Dozer - returning managed state objects will cause lots of troubles. – nike.laos Dec 30 '16 at 20:16
1
  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;
}
nike.laos
  • 304
  • 2
  • 6
  • For _1._ if you read the docs, you see that Spring Data JPA is integrated with MVC to work like this. Actually _1._ is the part that works. – βξhrαng Dec 31 '16 at 09:33
  • good catch, I didn't know that. Anyway, have you tried to debug your test? With @ModelAttribute you get persisted Person entity injected. I haven't seen where you have request body with new Person state and where you merge this entities. See my updated answer. – nike.laos Dec 31 '16 at 18:02
0

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.

Biju Kunjummen
  • 45,973
  • 14
  • 107
  • 121
  • I think the reason for two records is the import.sql under src/main/resources which has insert sql. If you remove the script there is only one row generated with id 1. If you look at the assertion `Expected: "Tin Tin" but: was "Tin"` you'll see that "Tin" is coming from the insert script. – s7vr Dec 28 '16 at 22:16
  • Yes, but he is expecting that record to be updated, hence the "PUT" call right? – Biju Kunjummen Dec 28 '16 at 22:19
  • 1
    Yes he is "PUT"ing to update the record already created, but because he doesn't have id mapped it creates a new entry. Makes sense. Thanks. – s7vr Dec 28 '16 at 22:29
  • _The issue is that when you call personRepository.save(person) your person entity does not have the primary key field(id) _. It has. I have uploaded sample code (see the question) that shows this is not the issue. – βξhrαng Dec 29 '16 at 07:03
  • I have tried your sample @Behrang, it does have the primary key field but the accessors(setters and getters) are not public and you are not populating it at the point of calling `personRepository.save`. You will see that in your version `id` of Person is not populated before you call `personRepository.save`. If it is correctly populated you will not see this issue. – Biju Kunjummen Dec 29 '16 at 09:46
  • @Behrang The person loaded with database has id 1 and now when you send the xml payload in the request it doesn't have any id field populated. So you see the disconnect. I will explain this in the context of Hibernate. Hibernate will load the person with id 1 into the persistent cache, but at the point of updating you don't have any information(id) for hibernate to look it up and see if it's already there and merge the fields. So basically you need to populate id for the whole save/upsert/update logic to kick in. – s7vr Dec 29 '16 at 12:21
  • @SagarReddy I think you might have misunderstood my question. What I have written in the _Expected Behavior_ section works fine with the `application/x-www-form-urlencoded` content type (See [this passing test](https://github.com/behrangsa/howto-springdata/blob/spring-boot-bug-report/src/test/java/org/behrang/howto/springdata/controllers/PersonControllerTest.java#L101)). The problem is: how to make it work with XML and/or JSON. Apparently [it is not possible](https://github.com/spring-projects/spring-boot/issues/7754#issuecomment-269563822). – βξhrαng Dec 29 '16 at 13:15
0

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.

Prateek Singh
  • 1,071
  • 9
  • 17