17

Since version 2.5.7 Spring Data REST does not properly perform a PUT request to update resource which has associated resources. Unlike PATCH request that works as expected!

For example, Person has a many-to-one association with Addres. If we perform a PUT request with SDR v.2.5.6 (Spring Boot v.1.4.3) then all works OK. But if we switch to version 2.5.7 (i.e. to Spring Boot v.1.4.4) then we get an error:

Can not construct instance of Address: no String-argument constructor/factory method to deserialize from String value

The same happens with other types of associations, for example with one-to-many (uni- and bidirectional) - see my example application code and tests.

This problem is present in all versions of the Spring Boot since 1.4.4 including the latest stable 1.5.6 version, as well as the newest 2.0.0-SNAPSHOT version!

To work around this situation we can just switch to SDR v.2.5.6 (Spring Boot v.1.4.3).

I've prepared a Postman collection of requests to help you play with the issue: SDR PUT Issue

UPDATE 2017-08-14

I found how to avoid the error Can not construct instance of Address: no String-argument constructor/factory method to deserialize from String value.

Since I'm using Lombok in this project, it is necessary just to tell Lombok to suppress using the @ConstructorProperties annotation in generated constructors. So I set lombok.anyConstructor.suppressConstructorProperties=true in the 'lombok.config' file and the error was gone.

Unfortunately a new problem was found - PUT request does not update associated objects at all!

The example below is demonstrating this. When we are trying to update Person by changing his Address from addresses/1 (initial value) to addresses/2 - then it remains the same: addresses/1! As well as the previous problem this one is present in all versions of the Spring Boot since 1.4.4 (SDR - from v.2.5.7).

I debugged my project and found out that the reason of the issue is hidden in the method DomainObjectReader#mergeForPut (see its source) - it never replaces associated resources with new ones.

Before I post this issue on Spring JIRA, please report here if you have this issue in your projects and what do you think about it.

You can get my test here and check it in your projects - the test is 'standalone' and doesn't depend from other classes/modules (exclude only H2, I hope).

@Entity
public class Person {

    private String name;

    @ManyToOne
    private Address address;

    // other stuff
}

@Entity    
public class Address {

    private String street;

    // other stuff
}

Trying to update Person:

PUT http://localhost:8080/api/persons/1
{
    "name": "person1u",
    "address": "http://localhost:8080/api/addresses/2"
}

Getting the correct response:

{
    "name": "person1u",
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/persons/1"
        },
        "person": {
            "href": "http://localhost:8080/api/persons/1"
        },
        "address": {
            "href": "http://localhost:8080/api/persons/1/address"
        }
    }
}

Then checking for a 'new' Address of Person - address was not updated:

GET http://localhost:8080/api/persons/1/address
{
    "street": "address1",
    "_links": {
        "self": {
            "href": "http://localhost:8080/api/addresses/1"
        },
        "address": {
            "href": "http://localhost:8080/api/addresses/1"
        }
    }
}

UPDATE 2017-08-24

Thanks to Scott C. answer, it turned out that SDR has a bug, which is described in two tickets: DATAREST-1001 and DATAREST-1012.

Cepr0
  • 28,144
  • 8
  • 75
  • 101
  • what's the object is for the link `http://localhost:8080/api/persons/1/address` (v.2.5.6)? – Andrew Tobilko Aug 10 '17 at 17:40
  • @AndrewTobilko address1: `{ "street": "address1", "_links": { "self": { "href": "http://localhost:8080/api/addresses/1" }, "address": { "href": "http://localhost:8080/api/addresses/1" } } }` – Cepr0 Aug 10 '17 at 17:43
  • I didn't get why it works with the first version. The same exception should have been thrown because an `Address` instance can't construct from a single `String`. What is the `BaseEntity`? – Andrew Tobilko Aug 10 '17 at 17:46
  • @AndrewTobilko It's a [base class](https://github.com/Cepr0/put-doesnt-work-in-sdr/blob/master/src/main/java/io/github/cepr0/putissue/BaseEntity.java) for entities. All ctors are present - I use Lombok in my projects... – Cepr0 Aug 10 '17 at 17:50
  • @AndrewTobilko About `http://localhost:8080/api/persons/1/address` (v.2.5.6) - after PUT request there is 'address2' in there. – Cepr0 Aug 10 '17 at 17:58
  • Hi there. A number of women in our community sometimes say that every time they see gendered assumptions about software engineers, they feel a bit excluded. I wonder, could you try to avoid adding male-oriented greetings and pronouns in your posts, so as to make for a more welcoming environment? Thank you. – halfer Aug 14 '17 at 23:07
  • 1
    @halfer OK. Thanks. – Cepr0 Aug 15 '17 at 05:30
  • Why are you even trying to use PUT? Are you experiencing an issue with PATCH? Its generally safer for an update procedures than PUT. – wheeleruniverse Aug 18 '17 at 23:35
  • @jDub9 PATCH works fine in all versions, but PUT - doesn't. Before post a bug report on the Spring Jira I'd like to make sure that I was not mistaken, and this bug was revealed not only by me. – Cepr0 Aug 19 '17 at 05:49
  • To be clear, your current issue is that you're PUTing to /person/{id}/address with a json document containing the person and the associated addresses and it's persisting the person but not the addresses? – Ben M Aug 22 '17 at 21:03
  • @Ben Not quite so. I'm PUTing a new content of `/persons/1`: a new `name` and a new `address`. Then SDR is updating `name` but isn't updating `address`. – Cepr0 Aug 22 '17 at 21:10
  • This bug has described in [DATAREST-1061](https://jira.spring.io/browse/DATAREST-1061) too. – Dmitry Stolbov Sep 01 '17 at 06:43
  • @DmitryStolbov thanks for the note. – Cepr0 Sep 01 '17 at 06:59

2 Answers2

8

Looks like the issue has already been reported as a bug: - please verify. As best as I can tell, this is the issue you are reporting above.

Note, I am revising my previous answer to be this bug report.

Scott C.
  • 3,672
  • 4
  • 18
  • 20
  • Thank you for your response! I know deference between PUT and PATCH. And you absolutely right - PUT must replace the entity by the new one, entirely, both simple fields and references to other objects. As [RFC2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.6) said: the **enclosed entity** SHOULD be considered as a **modified version of the one residing on the origin server**. As you can see the field 'name' of `Person` object is replaced with the new 'person1u' value but `address` reference - isn't. So I think - this is really a bug which appeared in SB from v. 1.4.4 – Cepr0 Aug 22 '17 at 06:01
  • I am not sure if it is the same issue as here. Yes, it might be related, but the bug you linked is only about collections, while this bug affects even non-collection resource associations (e.g. @ManyToOne relation) – Adam Kučera Aug 23 '17 at 12:55
  • The reason why I linked that ticket is because it appears to be the ticket that links to others for the same area of code. If you look at the comments in the ticket referenced you will see this ticket too https://jira.spring.io/plugins/servlet/mobile#issue/DATAREST-1001 which is more directly related. That said, I think the above ticket is what needs to be fixed to resolve this issue. – Scott C. Aug 24 '17 at 02:15
  • Thank you! Your updated answer is solving this question - SDR has a bug in the PUT handling (It's strange that I did not find these tickets myself). – Cepr0 Aug 24 '17 at 07:32
  • For future reference and how I found the bug report, I went to Jira searched for Put and it returned ~70 issues. I read the summary of several tickets. When I found a candidate ticket I read the whole ticket to compare to your problem. Knowing the area of code helped so the research done in the OP was helpful. It took time to find it, no doubt. – Scott C. Aug 24 '17 at 10:40
3

I agree with you that this is a bug in Spring Data REST and it should be reported.

I have the same issue in my project, where updating an entity via PATCH request works fine, but PUT request updates only fields of the given entity, but not its associated resources.

Why do I consider this a bug?

  • As people have correctly pointed out, PUT should be used for replacing a resource as a whole with a modified version, which suggests that it should be working, if you provide all fields for the resource (as in a POST request). However, in current Spring Data REST version, only the simple fields of an entity are updated and the associated resources are left intact which makes PUT request only "partially working" and that is definately not an expected behavior (even if it fulfills the RFC).
  • Moreover, Spring Data REST even allows you to do a partial PUT request, i.e. updating just your name field via PUT works. It does not work for the address though.
  • It means that Spring Data REST does not work exactly how the RFC specifies it (which may be for another debate), however it also does not provide a consistent usage - when updating one field works and updating other does not without any sign of an error.

For the record, I am using Spring Data REST 2.6.3.

Adam Kučera
  • 424
  • 5
  • 15
  • Thank you for your opinion! I already began to doubt the objectivity of this issue... )) – Cepr0 Aug 23 '17 at 07:00
  • At this [presentation](https://speakerdeck.com/olivergierke/hypermedia-apis-with-spring-5?slide=20) Olver Gierke don't use PUT to update/replace resources at all. Maybe we don't know something?.. )) – Cepr0 Aug 23 '17 at 07:25
  • Well from the [link](https://stackoverflow.com/questions/28459418/rest-api-put-vs-patch-with-real-life-examples) Scott C. posted it actually seem that PUT is not very useful after PATCH was introduced. The only reasonable use case seems to be when you require the request to be idempotent. So maybe PATCH should be prefered for _common_ updating. But it does not change the fact it is a bug, as PUT does not work as expected. – Adam Kučera Aug 23 '17 at 07:35
  • Can you please link the issue after you post it to the Spring Data REST JIRA? – Adam Kučera Aug 23 '17 at 07:37
  • Of cause, I'm going to post it here. – Cepr0 Aug 23 '17 at 08:11
  • Could you check how SDR updates (with PUT) entities with bidirectional associations, for example one-to-many? In my project I got an exception: `IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : Child.parent -> Parent` – Cepr0 Aug 23 '17 at 08:32
  • And naturally PATCH updates bidirectional associations as expected.. )) – Cepr0 Aug 23 '17 at 09:03
  • Scott C. has found Jira tickets that already described this bug (I'v update my question) – Cepr0 Aug 24 '17 at 07:44