2

I am new to Spring, and I am trying to create a RESTful resource to use on my API. So far, I was able to list elements (GET /people.json), show a specific element (GET /people/{id}.json), create a new element (POST /people.json), and delete an element (DELETE /people/{id}.json). My last step is to work on the update method (PUT /people/{id}.json, but I have no idea how to do it yet.

Here is the code I have for my controller:

package com.example.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.example.model.Person;
import com.example.service.PersonService;

import java.util.Map;

@Controller
public class PersonController {

    @Autowired
    private PersonService personService;

    @ResponseBody
    @RequestMapping(value = "/people.json", method = RequestMethod.GET)
    public String all(Map<String, Object> map) {
        return "{\"people\": " + personService.all() + "}";
    }

    @ResponseBody
    @RequestMapping(value = "/people/{personId}.json", method = RequestMethod.GET)
    public String show(@PathVariable("personId") Integer personId) {
        Person person = personService.find(personId);
        return "{\"person\": "+person+"}";
    }


    @ResponseBody
    @RequestMapping(value = "/people.json", method = RequestMethod.POST)
    public String create(@ModelAttribute("person") Person person, BindingResult result) {
        personService.create(person);
        return "{\"person\": "+person+"}";
    }

    @ResponseBody
    @RequestMapping(value = "/people/{personId}.json", method = RequestMethod.PUT)
    public String update(@PathVariable("personId") Integer personId, @ModelAttribute("person") Person person, BindingResult result) {
        personService.update(personId, person);
        return "{\"person\": "+person+"}";
    }    

    @ResponseBody
    @RequestMapping(value = "/people/{personId}.json", method = RequestMethod.DELETE)
    public String delete(@PathVariable("personId") Integer personId) {
        personService.delete(personId);
        return "{\"status\": \"ok\", \"message\": \"\"}";
    }
}

And here is the code for my service:

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.model.Person;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;

import java.util.List;

@Service
public class PersonServiceImpl implements PersonService {

    @PersistenceContext
    EntityManager em;

    @Transactional
    public List<Person> all() {
        CriteriaQuery<Person> c = em.getCriteriaBuilder().createQuery(Person.class);
        c.from(Person.class);
        return em.createQuery(c).getResultList();
    }

    @Transactional
    public Person find(Integer id) {
        Person person = em.find(Person.class, id);
        return person;
    }

    @Transactional
    public void create(Person person) {
        em.persist(person);
    }

    public void update(Integer id, Person person) {
        // TODO: How to implement this?
    }

    @Transactional
    public void delete(Integer id) {
        Person person = em.find(Person.class, id);
        if (null != person) {
            em.remove(person);
        }
    }   
}

So, for the Spring, hibernate experts out there, what is the best way to accomplish what I need? It is also important that the resource update only updates the actual changed attributes

Thanks,

marcelorocks
  • 2,005
  • 3
  • 18
  • 28

1 Answers1

1

Change your PersonService update method signature so that it returns a Person, also, you don't need an separate id, just use the Person object to hold the id. Them implement the method like this:

public Person update(Person person) {
    // you might want to validate the person object somehow
    return em.merge(person);
}

Also update your web service layer accordingly. Either change it so that personId is no longer there, or keep it and set the Person id with it:

public String update(@PathVariable("personId") Integer personId, 
        @ModelAttribute("person") Person person, BindingResult result) {
    person.setId(personId);
    Person updated = personService.update(person);
    // You may want to use a JSON library instead of overriding toString.
    return "{\"person\": " + updated + "}";
}   

One small gotcha, merge may also persist new entities. So, if you want to make sure that update only updates existing ids, change its body to something like:

public Person update(Person person) {
    if(em.find(Person.class, person.getId()) == null) {
        throw new IllegalArgumentException("Person " + person.getId() 
                + " does not exists");
    }
    return em.merge(person);
}

It is also important that the resource update only updates the actual changed attributes

If you want hibernate to update only attributes that are different between your JSON object and the database there is a handy combo of dynamicUpdate and selectBeforeUpdate properties on @org.hibernate.annotations.Entity (check out the Hibernate Manual). Only enable this if you really need the behavior (which is non standard behavior and may decrease performance).

@org.hibernate.annotations.Entity(dynamicUpdate = true, selectBeforeUpdate = true)

On Hibernate 4.0 those properties where deprecated, you can use individual annotations instead:

@DynamicUpdate
@SelectBeforeUpdate
Anthony Accioly
  • 21,918
  • 9
  • 70
  • 118
  • Thank you, Anthony. This looks like the answer, but maybe something is still missing. When I call put on the API with the paramters {"firstName": "John"} I get back the json with nullified firstname and lastname (although id doesnt). The record is also not being updated in the database. Do I need to set all the other parameters on the person object? – marcelorocks Mar 04 '14 at 17:55
  • marcel, you are sending a partial object with several `null` properties, Hibernate interprets this as "update the record setting null for all of the missing fields. If you id does not exists it will insert a new record. – Anthony Accioly Mar 04 '14 at 18:15
  • See my update for how to handle dynamic updates. If it does not cut it you are stuck with querying the last persisted entity state (`find`) and updating non null properties by hand: `if (person.getFirstName() != null && !dbPerson.getFirstName().equals(person.getFirstName())) dbPerson.setFirstName(person.getFirstName());` but this is ugly and verbose code. – Anthony Accioly Mar 04 '14 at 18:25
  • Anthony, I got your updates, thanks. However, the problem seems to be before that. My controller method signature: public String update(@PathVariable("personId") Integer personId, @ModelAttribute("person") Person person, BindingResult result) may not be right, because when I print the person object it does not have the firstName or lastName populated. It works on my create method, so I wonder if this is the right way to do the update (the difference between them is the personId path variable) – marcelorocks Mar 04 '14 at 18:28
  • Id comes in just fine. Here is exactly what I am doing: PUT "http://localhost:8080/api/people/7.json", {firstName: "Lara", lastName: "Croft"} And person object comes with firstName null and lastName null. – marcelorocks Mar 04 '14 at 18:34
  • This can happen because of lots of reasons. You can try replacing [@ModelAttribute with @RequestBody](http://stackoverflow.com/questions/19600532/modelattribute-for-restful-put-method-not-populated-value-json) and including Jackson in your class library for parsing. Or [use a custom filter](http://stackoverflow.com/a/13784968/664577) for `'application/x-www-form-urlencoded` PUT calls. – Anthony Accioly Mar 04 '14 at 19:39
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/48991/discussion-between-anthony-accioly-and-marcelorocks) – Anthony Accioly Mar 04 '14 at 19:39