Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "EclipseLink/UserGuide/JPA/Advanced JPA Development/JPA RESTful Service"

m
m (Configuration Files)
Line 509: Line 509:
  
 
* JAXB - You must create the necessary artifacts for JAXB:
 
* JAXB - You must create the necessary artifacts for JAXB:
** <tt>jaxb.properties</t> file to specify JAXB implementation
+
** <tt>jaxb.properties</tt> file to specify JAXB implementation
 
** <tt>eclipselink-oxm.xml</tt> as an alternate metadata representation
 
** <tt>eclipselink-oxm.xml</tt> as an alternate metadata representation
  

Revision as of 14:05, 27 January 2011

EclipseLink JPA

Eclipselink-logo.gif
EclipseLink
Website
Download
Community
Mailing ListForumsIRCmattermost
Issues
OpenHelp WantedBug Day
Contribute
Browse Source

Expose JPA Entities Through a RESTful Service

This page is in progress... .


This page describes how to expose JPA entities through a a standards-based (JAX-RS/JAXB/JPA) RESTful service. REST is an acronum for REpresentational State Transfer (REST), which is a design idiom that embraces a stateless client-server architecture in which Web services are viewed as resources which can be identified by their URLs.

Implement a JPA-based JAX-RS service as follows. Extend JPASingleKeyResource or JPACompositeKeyResource depending upon the key type.

import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.ws.rs.Path;
 
import org.eclipse.persistence.jpa.rest.JPASingleKeyResource;
 
@Stateless
@LocalBean
@Path("/customers")
public class CustomerService extends JPASingleKeyResource<Customer, Long> {
 
    @PersistenceContext(unitName="CustomerService", type=PersistenceContextType.TRANSACTION)
    EntityManager entityManager;
 
    public CustomerService() {
        super(Customer.class);
    }
 
    @Override
    protected EntityManager entityManager() {
        return entityManager;
    }
 
}


URI Representation

Data in a RESTful service is referenced through URIs. The common parts of the URI used for the example in this section are:

  • http://www.example.com/customer-app/rest - The first part of the URI is based on how the application is deployed.
  • customers - This part of the path corresponds to the JAX-RS @Path annotation on the RESTful service.

Other aspects of URI construction are discussed in the following sections:

URI for JPA Entities with Unary Key

If the JPA entity has a single part primary key, the primary key is represented as a path parameter in the corresponding URI. This is a common RESTful operation.

@Entity
public class Customer implements Serializable {
 
    @Id
    private long id;
 
}

The URI corresponding to the Customer entity with id == 1 is:

http://www.example.com/customer-app/rest/customers/1

URI for JPA Entities with Composite Keys

A different mechanism is used when locating a resource with composite keys. The URI leverages the property names from the JPA key class as matrix parameters. The advantage of using matrix parameters is that they may be cached. The same representation is also used if composite keys are represented using an embedded key class.

@Entity
@IdClass(CustomerID.class)
public class Customer implements Serializable {
 
    @Id
    private long id;
 
    @Id
    private String country;
 
}
public class CustomerID {
 
    private String country;
    private long id;
 
    public CustomerID() {
        super();
    }
 
    public CustomerID(String country, long id) {
        this.country = country;
        this.id = id;
    }
 
    public String getCountry() {
        return country;
    }
 
    public long getId() {
        return id;
    }
 
}

The URI corresponding to the instance of Customer with id == 1 and country == CA is:

http://www.example.com/customer-app/rest/customers;id=1;country=CA

Named Queries: Read

A named read query call must be mapped to a URI. Below is an example named read query:

@NamedQuery(name = "findCustomerByName",
            query = "SELECT c " +
                    "FROM Customer c " +
                    "WHERE c.firstName = :firstName AND " +
                    "      c.lastName = :lastName")

The URI for retrieving a single result is, for example:

http://www.example.com/customer-app/rest/customers/singleResult/findCustomerByName;firstName=Jane;lastName=Doe

The URI for retrieving a result list is, for example:

http://www.example.com/customer-app/rest/customers/resultList/findCustomerByName;firstName=Jane;lastName=Doe?firstResult=1&maxResults=10

The components of the URI in the previous example are:

  • findCustomersByName - This corresponds to the name of the named query.
  • singleResult or resultList - This portion indicates whether one or many results are returned.
  • ;firstName=Jane;lastName=Doe - These are matrix parameters. The name of the parameter must match exactly the parameter name in the named query.
  • ?firstResult=1&maxResults=10 - These are optional query parameters to specify firstResult and maxResults.

The parameters are used to build the equivalent of the following:

Query query = entityManager.createNamedQuery("findCustomersByCity");
query.setParameter("firstName", "Jane");
query.setParameter("lastName", "Doe");
query.setFirstResult(1);
query.setMaxResults(10);
return query.getResultList();

Named Queries: Update & Delete

A named update and delete query calls must be mapped to a URI. Below is an example named update query:

@NamedQuery(name = "updateCustomersByCity",
            query = "UPDATE Customer c " +
                    "SET c.address.city = :newCity " +
                    "WHERE c.address.city = :oldCity")

Execute the query:

http://www.example.com/customer-app/rest/customers/execute/updateCustomersByCity;oldCity=Nepean;newCity=Ottawa

URI components:

  • updateCustomersByCity - This corresponds to the name of the named query.
  • execute - This portion indicates the query will be executed.
  • ;oldCity=Nepean;newCity=Ottawa - These are matrix parameters; the name of the parameter must match exactly the parameter name in the named query.

The parameters are to build the equivalent of the following:

Query query = entityManager.createNamedQuery("updateCustomersByCity");
query.setParameter("oldCity", "Nepean");
query.setParameter("newCity", "Ottawa");
query.executeUpdate();

REST (CRUD) Operations

POST - Create Operation

Using the Jersery client APIs, the following is how a post operation is called on the EclipseLink service. The XML message will converted to the appropriate object type using JAXB.

Client c = Client.create();
WebResource resource = client.resource("http://www.example.com/customer-app/rest/customers");
ClientResponse response = resource.type("application/xml").post(ClientResponse.class, "<customer>...</customer>");
System.out.println(response);

This call will be received by

@POST
@Consumes({"application/xml", "application/json"})
public Response create(@Context UriInfo uriInfo, EntityType entity) {
    entityManager().persist(entity);
    UriBuilder uriBuilder = pkUriBuilder(uriInfo.getAbsolutePathBuilder(), entity);
    return Response.created(uriBuilder.build()).build();
}

Successful Responses

  • 200 OK
  • Return the URI (the representation discussed earlier) for the created entity.

Error Responses:

  •  ?

GET - Read Operation

Get is a read-only operation. It is used to query resources. The following is an example of how to invoke a GET call using the Jersey client APIs:

WebResource resource = client.resource("http://www.example.com/customer-app/rest/customers;id=1;country=CA");
ClientResponse response = resource.accept(mimeType).get(ClientResponse.class);

We will need to differentiate between the single key case that uses path parameters and the composite key case that uses matrix parameters:

Single Key - Path Parameter

The unary key parameter will be passed directly to us. Note the String to KeyType conversion will be done using JAXB conversion rules and not JPA conversion rules. There should be no differences for common key types such as Strings and numeric types.

@GET
@Path("{id}")
@Produces({"application/xml", "application/json"})
public EntityType read(@PathParam("id") KeyType id) {
    return entityManager().find(entityClass, id);
}

Composite Key - Matrix Parameters

An instance of the primary key class will need to be derived from the matrix parameters. A utility will need to be provided for this.

@GET
@Produces({"application/xml", "application/json"})
public EntityType read(@Context UriInfo info) {
    return entityManager().find(entityClass, getPrimaryKey(info));
}

Successful Responses

  • 200 OK - If a result is returned
  • 204 No Content - If no results are returned

Error Responses:

  •  ?

PUT - Update Operation

The put operation updates the underlying resource. When using put the client knows the identity of the resource being updated. The following is an example of how to invoke a PUT call using the Jersey client APIs:

Client c = Client.create();
WebResource resource = client.resource("http://www.example.com/customer-app/rest/customers/1");
ClientResponse response = resource.type("application/xml").put(ClientResponse.class, "<customer>...</customer>");
System.out.println(response);

This call will be received by

@PUT
@Consumes({"application/xml", "application/json"})
public void update(EntityType entity) {
    entityManager().merge(entity);
}

Successful Responses

  • 200 OK

Error Responses:

  • 409 Conflict - Locking related exception

DELETE - Delete Operation

The delete operation is used to remove resources. It is not an error to remove a non-existent resource. Below is an example using the Jersey client APIs:

WebResource resource = client.resource("http://www.example.com/customer-app/rest/customers/1");
ClientResponse response = resource.delete(ClientResponse.class);

We will need to differentiate between the single key case that uses path parameters and the composite key case that uses matrix parameters:

Single Key - Path Parameter

@DELETE
@Path("{id}")
public void delete(@PathParam("id") KeyType id) {
    super.delete(id);
}

Composite Key - Matrix Parameters

An instance of the primary key class will need to be derived from the matrix parameters. A utility will need to be provided for this.

@DELETE
public void delete(@Context UriInfo info) {
    super.delete(getPrimaryKey(info));
}

Successful Responses

  • 200 OK

Error Responses:

Matrix Parameters to Instance of ID Class

The matrix parameters could be converted to an ID class in the following manner (Note the code below is currently using query parameters and needs to be updated):

private KeyType getPrimaryKey(UriInfo info) {
    try {
        KeyType pk = (KeyType) PrivilegedAccessHelper.newInstanceFromClass(keyClass);
        for(Entry<String, List<String>> entry : info.getQueryParameters().entrySet()) {
            Field pkField = PrivilegedAccessHelper.getField(keyClass, entry.getKey(), true);
            Object pkValue = ConversionManager.getDefaultManager().convertObject(entry.getValue().get(0), pkField.getType());
            PrivilegedAccessHelper.setValueInField(pkField, pk, pkValue);
        }
        return pk;
    } catch(Exception e) {
        throw new RuntimeException(e);
    }
}

The key class can be obtained using the the JPA metamodel facility:

keyClass = (Class<KeyType>) entityManager().getMetamodel().entity(entityClass).getIdType().getJavaType();


API

Much of the shared behavior is available in the super class:

package org.eclipse.persistence.jpa.rest;
 
import java.util.List;
import java.util.Map.Entry;
 
import javax.persistence.*;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
 
public abstract class JPAResource<EntityType, KeyType> {
 
    protected Class<EntityType> entityClass;
 
    public JPAResource(Class<EntityType> entityClass) {
        this.entityClass = entityClass;
    }
 
    protected abstract EntityManager entityManager();
 
    @POST
    @Consumes({"application/xml", "application/json"})
    public Response create(@Context UriInfo uriInfo, EntityType entity) {
        ...
    }
 
    @GET
    @Path("singleResult/{namedQuery}")
    @Produces({"application/xml", "application/json"})
    public EntityType namedQuerySingleResult(@Context UriInfo info) {
        ...
    }
 
    protected List<EntityType> namedQueryResultList(@Context UriInfo info) {
        ...
    }
 
    @PUT
    @Consumes({"application/xml", "application/json"})
    public void update(EntityType entity) {
        ...
    }
 
    public void delete(KeyType id) {
    }
 
    protected abstract UriBuilder pkUriBuilder(UriBuilder uriBuilder, EntityType entity);
 
}

A specialized class is available for a service based on a single key entity:

package org.eclipse.persistence.jpa.rest;
 
import java.lang.reflect.Field;
 
import javax.persistence.metamodel.SingularAttribute;
import javax.ws.rs.*;
import javax.ws.rs.core.UriBuilder;
 
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
 
public abstract class JPASingleKeyResource<EntityType, KeyType> extends JPAResource<EntityType, KeyType> {
 
    public JPASingleKeyResource(Class<EntityType> entityClass) {
        super(entityClass);
    }
 
    @GET
    @Path("{id}")
    @Produces({"application/xml", "application/json"})
    public EntityType read(@PathParam("id") KeyType id) {
	...
    }
 
    @DELETE
    @Path("{id}")
    public void delete(@PathParam("id") KeyType id) {
        super.delete(id);
    }
 
    @Override
    protected UriBuilder pkUriBuilder(UriBuilder uriBuilder, EntityType entity) {
	...
    }
 
}

Another specialized class is available for a service based on a composite key entity:

package org.eclipse.persistence.jpa.rest;
 
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map.Entry;
 
import javax.ws.rs.*;
import javax.ws.rs.core.*;
 
import org.eclipse.persistence.internal.helper.ConversionManager;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
 
public abstract class JPACompositeKeyResource<EntityType, KeyType> extends JPAResource<EntityType, KeyType> {
 
    public JPACompositeKeyResource(Class<EntityType> entityClass) {
	...
    }
 
    @GET
    @Produces({"application/xml", "application/json"})
    public EntityType read(@Context UriInfo info) {
	...
    }
 
    @DELETE
    public void delete(@Context UriInfo info) {
	...
    }
 
    private KeyType getPrimaryKey(UriInfo info) {
	...
    }
 
    @Override
    protected UriBuilder pkUriBuilder(UriBuilder uriBuilder, EntityType entity) {
	...
    }
 
}

Configuration Files

No extra configuration files are required. However, you are responsible for providing the required JAX-RS, JPA, and JAXB configuration files:

  • JAX-RS - You must create the JAX-RS deployment artifacts appropriate to your deployment platform.
  • JPA - You must create the necessary artifacts for JPA.
  • JAXB - You must create the necessary artifacts for JAXB:
    • jaxb.properties file to specify JAXB implementation
    • eclipselink-oxm.xml as an alternate metadata representation



Eclipselink-logo.gif
Version: 2.2.0 DRAFT
Other versions...

Back to the top