Severin Severin - 4 months ago 49
Java Question

What is the proper REST URI for a JPA entity with a composite primary key in JAX-RS?

I have the following setting.

Technologies used:


  • NetBeans IDE

  • MySQL DB

  • EclipseLink JPA

  • JAX-RS



I generated my entities and rest services using the NetBeans tool "RESTful Web Services from Database. Now I have a view in my DB where a row is uniquely identified by a compount consisting of two columns (I know it makes no sense to speak of a primary key regarding views, but let's call it primary key anyway, for simplicity). I modeled the view as an entity with an embedded composite primary key as follows:

@Entity
@Table(name = "UserAmountView")
@XmlRootElement
public class UserAmountView implements Serializable {

private static final long serialVersionUID = 1L;
@EmbeddedId
protected UserAmountPK userAmountPK;

// fields, constructors, getters, setters etc.

}

@Embeddable
public class UserAmountPK implements Serializable {

@Basic(optional = false)
@NotNull
@Column(name = "UserID")
private int userID;
@Basic(optional = false)
@NotNull
@Column(name = "BalanceID")
private Integer balanceID;

public UserAmountPK() {
}

public UserAmountPK(int userID, Integer balanceID) {
this.userID = userID;
this.balanceID = balanceID;
}

// getters and setters etc...
}


Now my question is, how to I best RESTfully address an instance of such an entity? NetBeans generates a procedure using matrix parameters (one for each part/column of the composite key) for normal tables with composite primary keys, so I took this approach over for my view. I would expect, that I can GET the entity consisting of primary key (7, 3) under .../wgm.rest.useramountview;userID=7;balanceID=3, but of course this doesn't work since the matrix parameters are in the same path segement as wgm.rest.useramountview, so the request will just return all existing entities. Next I tried .../wgm.rest.useramountview/;userID=7;balanceID=3 which gives me the same result (why?). Only if I insert something in between the '/' and ';', then it works as expected and returns the entity identified by (7, 3), e.g. .../wgm.rest.useramountview/foo;userID=7;balanceID=3.
Obviously, I would like to avoid always having to insert a 'foo' in the URL. What are the alternatives?

My service class currently looks as follows:

@Stateless
@Path("wgm.rest.useramountview")
public class UserAmountViewFacadeREST extends AbstractFacade<UserAmountView> {

@PersistenceContext(unitName = "WGManagerPU")
private EntityManager em;

private UserAmountPK getPrimaryKey(PathSegment pathSegment) {
wgm.rest.UserAmountPK key = new wgm.rest.UserAmountPK();
javax.ws.rs.core.MultivaluedMap<String, String> map = pathSegment.getMatrixParameters();
java.util.List<String> userID = map.get("userID");
if (userID != null && !userID.isEmpty()) {
key.setUserID(new java.lang.Integer(userID.get(0)));
}
java.util.List<String> balanceID = map.get("balanceID");
if (balanceID != null && !balanceID.isEmpty()) {
key.setBalanceID(new java.lang.Integer(balanceID.get(0)));
}
return key;
}

@GET
@Path("{id}")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public UserAmountView find(@PathParam("id") PathSegment id) {
wgm.rest.UserAmountPK key = getPrimaryKey(id);
return super.find(key);
}

@GET
@Override
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public List<UserAmountView> findAll() {
return super.findAll();
}

// ...
}


Update:



Another possibility would be to include the single columns of the composite key as path params, like this:

@GET
@Path("{userID}/{balanceID}")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public UserAmountView find(
@PathParam("userID") String userID,
@PathParam("balanceID") String balanceID) {

wgm.rest.UserAmountPK key =
new UserAmountPK(new Integer(userID), new Integer(balanceID));
return super.find(key);
}


But still, this does not satisfy me because that would look like a subresource. In fact, both of the path parameters are on the same hierarchical level for identifying one resource.

Solution:



I finally came up with an adapted version of Roman Vottner's answer (see below): Using matrix parameters directly on the base resource path and differentiating between the two cases directly inside the method. I had to wrap the result in a Response object as the return types do not match (
UserAmountView
for
find
vs.
List<UserAmountView>
for
findAll
). The result looks like this:

@Stateless
@Path("wgm.rest.useramountview")
public class UserAmountViewFacadeREST extends AbstractFacade<UserAmountView> {

@PersistenceContext(unitName = "WGManagerPU")
private EntityManager em;
@GET
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response findAll(
@MatrixParam("userID") String userID,
@MatrixParam("balanceID") String balanceID) {
if (userID != null && balanceID != null) {
wgm.rest.UserAmountPK key = new UserAmountPK(Parser.asIntOrNull(userID), Parser.asIntOrNull(
balanceID));
return Response.ok(super.find(key)).build();
} else {
return Response.ok(super.findAll()).build();
}
}
}

Answer

You pass your compound key as matrix parameter .../wgm.rest.useramountview;userID=7;balanceID=3 but you have a defined path structures as either .../wgm.rest.useramountview or .../wgm.rest.useramountview/{id} where id is also defined as path parameter.

As matrix parameters belong to the resource they are defined on, JAX-RS will invoke the closest possible resource which is the findAll method on using a URI like .../wgm.rest.useramountview;userID=7;balanceID=3 though here you do not extract the matrix parameters. This is the reason why all entries are returned instead of the one specific on invoking that URI.

The second call .../wgm.rest.useramountview/foo;userID=7;balanceID=3 succeeds as you now provide the ID foo;userID=7;balanceID=3 to the service which JAX-RS will now resolve to the find(...) method. As you use a PathSegment object here, you simply ignore the actual ID value but just collect the matrix parameter defined on it.

To deal with matrix parameters without having to use some additional currently unused ID path parameter you should refactor the find and findAll methods to something like:

@GET
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public UserAmountView find(@MatrixParam("userID") Integer userID,
                           @MatrixParam("balanceID") Integer balanceID) {
    if (null != userID && null != balanceID) {
        return findOne(userID, balanceID);
    } else {
        return findAll();
    }
} 

As with PathParam or QueryParam you can also directly inject the MatrixParam as depicted in the example above. You can also re-use the PathSegment on the findAll method directly and retrieve the matrix parameters as you do, but I guess that using the @MatrixParam annotation is a bit more intuitive.