PUT method (RESTful) doesn't work as a way to update resources

12,225

The idea behind "HTTP Verbs" like PUT, GET, POST, DELETE are just a matter of protocol semantics. Just performing an HTTP PUT operation doesn't do anything magical. It's just proper semantics we as developers should understand, while developing, as these semantics are known to all (that's why protocols exist). If no one followed these semantics, the world would be somewhere between the Great Depression and the Apocalypse.

That being said, these verbs (semantics) are a sort of guarantee (or maybe assurance is a better word) to the client performing the request with a certain verb will have some know semantics to it. One major factor is the idea of idempotence. Idempotence is the idea that no matter how many times I make a request, the result will be the same (or have the same effect).

Certain HTTP verbs are said to be idempotent, such as PUT, DELETE, GET. No matter how many times be make the exact same request, the general idea is that the result/effect should be the same. POST on the other hand is said to not be idempotent, as the exact same POST request may produce different results, for example submit an order, wrongfully, again, or creating a new customer twice.

If we want to make the world a better place, and do our part in saving the world from a complete meltdown, we should learn these semantics and be good citizens by following them. There's a lot more to learn about the verb semantics, than just idempotence, but understanding that much, is a good start. I'd suggest maybe picking up a good book on REST to learn some good practices. Or if you want you want to be a cool kid, take time to read the bible (actually the Fielding Dissertation).

All that being said, it's our job as developers to create the code to follow these semantics. The reason your method is creating a new resource, is probably because you are creating a new resource with your code. Maybe something like this would seem more appropriate:

@PUT
@Path("/customers/{id}")
@Consumes(MediaType.APPLICATION_JSON)
public Response updateCustomer(@PathParam("id") long id, 
                               Customer updateCustomer) {

    Customer customer = customerService.getCustomerById(id);
    if (customer == null) {
        throw new WebApplicationException("Can't find it", 404);
    }

    customer.setFirstName(updateCustomer.getFirstName());
    customer.setLastName(updateCustomer.getLastName());
    ...

    return Response.noContent().build();
}

So we are just update the customer that already exists in our database. Normally with a PUT request to update, the particular customer resource URI should be known. So say the client makes a request to http://blah.com/api/customers/1234, our service will look up the customer with the id 1234. If it can't be found, we return a 404 status code, as the resource doesn't exist. If it does exist, then we update the customer with the customer data provided in the request. If you wanted to create a new customer, where the URI is not known, then POST would be correct, and you'd send a customer representation to http://blah.com/api/customers.

Also keep just an FYI: in many cases a case like this, what happens is that the client requests (GET) a resource, say a customer, and updates that customer representation, then send it back as PUT request with the updated customer. On the sever it should use that information to update the particular customer's data, as you can see from the example above.


UPDATE

Per your edit. You are completely missing the point of how this is supposed to work.

Customer cust = new Customer();
cust.setName("After");
cust.setPostcode(222);

target.path("Before").request().put(Entity.xml(cust));

What's wrong with this is that with the new Customer, you are setting the identifier to "After", which is different from the identifier in the request path, you are using "Before". So the path variable {id} is "Before". With this request URI you are saying that you want to access the customer with id "Before". As seen in my code, it's your duty to check if a customer with the id "Before" exists in the database. If not, you should return back a 404 Not Found. The name (id) you set for the new Customer should be the id expected in the database. So if you want to update the customer with id in the databse "After". then you should put "After" in the path, instead of "Before". We should not try and change the identifier.

Like I said, when we want to update a resource, we normally, GET the resource, update some field (but not the identifier), and send it back. A sequence might look something like

final String PATH = "http://hello.com/api/customers"
WebTarget target = client.target(PATH);
Customer customer = target.path("1234").request().get(Customer.class);
// where 1234 is the id (or in your case `name` of the customer.
// I would avoid using the name as the DB id, that's why my example uses numbers
customer.setPostalCode(...);
target = client.target(PATH).path(customer.getName()); // getName should be 1234
Response response = target.request().put(Entity.xml(customer));

We are using the same id as we were provided with, in the path, because that is the how the resource is identified in the server.

Share:
12,225
Hiroki
Author by

Hiroki

Updated on June 09, 2022

Comments

  • Hiroki
    Hiroki almost 2 years

    According to this article(http://restcookbook.com/HTTP%20Methods/put-vs-post/), PUT is supposed to work as a method to update resources.

    However, practicing RESTful with JAX_RS 2.0 and Jersey 2.0, I don't think it updates a particular resource. (I.e. I'm studying RESTful with JAX_RS 2.0 and Jersey 2.0)

    Here is a resouce like this.

    <customer>
        <name>Before</name>
        <postcode>111</postcode>
    </customer>
    

    What I'm trying to do is to update (perhaps I should say "replace") this resource.

        ClientConfig config = new ClientConfig();
        Client client = ClientBuilder.newClient(config);
        WebTarget target =  client.target("http://xxx/yyy/zzz/end.cust");
    
        Customer cust = new Customer();
        cust.setName("After");
        cust.setPostcode(222);
    
        target.path("Before").request().put(Entity.xml(cust));
    

    @Id annotation is set to "Name" in the "Customer" class, so the path "Before" is supposed to work as the ID and the first resource (named "Before") should be replaced with the second resource (named "After").

    However, after the coding above is executed, the "Before" resource still remains, and there is a new "After" resrouce. It seems that the PUT method worked to create a new resource, instead of updating something. (i.e. There are both "Before" and "After" resources, and nothing has been updated)

    I tested a POST method in order to create a new resource, and it created a new resource as I expected.

    If you see anything I'm doing wrong or what needs to be done, could you please give some advice?

    edit

    I'll add the server side code. The method annotated with @PUT is like this.

     @PUT
     @Path("{id}")
     @Consumes({"application/xml", "application/json"})
     public void edit(@PathParam("id") String id, Customer entity) {
         super.edit(entity);
     }
    

    This is inside a class called CustomerFacadeREST.java, automatically created after I created a "RESTful service from Database".

    According to NetBeans' document, super.edit() method is originally like this.

     public void edit(T entity) {
     getEntityManager().merge(entity);
     }
    

    In the "Customer" class, @Id is set to the "name" value in this way.

     public class Customer implements Serializable {
         private static final long serialVersionUID = 1L;
         @Id
         @Basic(optional = false)
         @NotNull
         @Size(min = 1, max = 80)
         @Column(name = "Name")
         private String name;
         // Other fields, such as Postcode...
    
         public Customer() {
         }
    
         // Other constructors and methods...
         }