Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I implement this REST API and stay DRY?

I'm building a REST API for performing CRUD operations on a database. My tentative stack is Jersey, Spring, Spring Data, JPA and Hibernate. I'm also using jersey-spring to supply instances of the resource class so Spring can autowire them.

The API will support CRUD operations on dozens of tables, with concomitant JPA Entities and DAOs backed by Spring Data repositories. The family of DAO interfaces and related DTOs looks something like this:

public interface CrudService<T extends PersistedObject> { /* ... */  }
public interface PersonService extends CrudService<Person> { /* ... */  }

public class PersistedObject { /* ... */ }
public class Person extends PersistedObject { /* ... */ }

Here's a simplified version of a JAX-RS resource class:

@Component
@Path("/people")
public class PersonResource {

    @Autowired
    private PersonService personService;

    @Path("/{id}")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Person get(@PathParam("id") String id) {
        return personService.findOne(Long.valueOf(id));
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response post(Person person) {
        personService.save(person);
        return Response.created().build();
    }
}

The problem is that remainder of the dozens of resource classes look almost identical, the only difference being they operate on a different PersistedObject subclass and its corresponding DAO. I'd like to stay DRY by having one resource class that can support the CRUD operations on all entity types, presumably via polymoprhism and clever injection of the DAO. It might look something like this:

@Component
@Path("/{resourceType}")
public class CrudResource {

    @Autowired
    private CrudService crudService;

    @Path("/{id}")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public PersistedObject get(@PathParam("id") String id) {
        return crudService.findOne(Long.valueOf(id));
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response post(PersistedObject entity) {
        crudService.save(entity);
        return Response.created().build();
    }
}

The issues I need to solve:

  • If resource methods accept and return PersistedObject, how will Jersey/Jackson know how to serialize/deserialize? The resourceType variable in the path parameter indicates the concrete type the user is requesting, but I don't know how to wield this to any advantage.
  • When Spring supplies the resource class instance, how will it know which DAO to inject?

Overall, I'm not sure that I'm on the right path. Is it possible to implement this in a generic way?

like image 906
Jeremy Ross Avatar asked Mar 07 '13 03:03

Jeremy Ross


2 Answers

I've run into this problem myself a few times. You can create a generic endpoint and a PersistedObjectDao and it should all work just fine. In Hibernate, session methods such as persist(), merge(), and delete() don't care about what it gets so long as it is a managed object or can become a managed object (in the case of merge()). Since you're only finding by id, and that should be managed in the PersistedObject class rather than the Person class, the functionality of the DAO will work just fine.

The only problem with this approach is that it breaks documentation tools such as Enunciate and makes the resource urls require globally unique ids. /xxx/1 and /yyy/1 cannot coexist. The findOne method will return the same object for both. This means that you will have to use @Inheritance(strategy = InheritanceType.JOINED) to avoid id collisions and create a globally unique id column across all persisted entities in the database.

Because of this I generally create an AbstractPersistedObjectDAO class and implement persist(), merge(), and delete() and abstract the findOne() to a subclass to avoid the need to cast in code if I ever need to do more than CRUD. But I generally just eat the cost of the boilerplate on the endpoints so that I can produce the REST documentation with Enunciate and it gives me a class to take on additional methods in the future if needed.

like image 186
Joe Avatar answered Oct 30 '22 15:10

Joe


Not sure if you're tied to JAX-RS in any way but the Spring Data family of projects comes with the Spring Data REST module that automatically exposes entities managed by Spring Data repositories in a hypermedia-driven way. It's based on Spring MVC.

So you essentially get CRUD operations on the entities for free, query methods exposed transparently and the ability to tweak and tune close to everything according to your needs.

Here are some useful links you might want to browse for further information:

  • Reference documentation
  • Spring Data REST sample project - taken from the Spring Data book available here.
  • Spring RESTBucks - a more advanced sample showing how to augment the entities exported with custom more complex functionality to implement business processes in a hypermedia-driven way.
like image 35
Oliver Drotbohm Avatar answered Oct 30 '22 14:10

Oliver Drotbohm