Search |
||||||||||||||||||||
Exposing Domain Models through the RESTful Service Interface, Part 1
Thu, 2009-06-04
|
||||||||||||||||||||
| JPA | JAXB | |
|---|---|---|
| Relationship | Owner | Inverse Side |
| @OneToOne | @XmlElement | @XmlTransient |
| @OneToMany | @XmlTransient | @XmlElement |
| @ManyToOne | @XmlElement | @XmlTransient |
| @ManyToMany | @XmlTransient | @XmlTransient |
Another way to manage the visibility of the domain model data on
the service interface is through the JPA
Lazy Loading. The default loading strategy of JPA is Fetch.LAZY,
while the JAXB framework expects a graph of objects already loaded in
memory. So, if you just serialize an @Entity object with JAXB, any lazy-loaded reference to other entities will appear blank on the output XML,
and we want to avoid the use of getters and setters during our magic
conversion between entities and XML elements. The workaround for that is
to change the field loading to EAGER, modifying the JPA annotation, as
shown in the FpCertificate code. It may produce a larger result set and
may impact the application performance, so you should use it only
when you know in advance that the size of the resultant objects graph is
acceptable. Otherwise just leave the Fetch.LAZY and force the client
to do a second service call to compose the object graph by parts.
Now that we have our model annotated by JAXB, we can use the entities both for persistence and for serialization without any other modification. In the next section I show the code of a Data Access Layer used by the web service to expose the domain model on the web.
You can use several strategies to integrate the persistence layer
with the controller layer, including the injection
of the EntityManager in the controller classes or the adoption of the Data
Access Object pattern. I chose the DAO option, and the steps below
enumerate the implementation of the data access layer as a set of EJB
3.0 Stateless Beans.
Define a CRUD interface that can handle the common
persistence operations with our entities. Notice the count
method, useful for pagination.
import java.io.Serializable;
import java.util.*;
import javax.persistence.*;
public interface
FpEntityFacade<T extends AbstractFootprintEntity> {
T create(T entity) throws Exception;
T read(Serializable primaryKey) throws Exception;
T update(T entity) throws Exception;
void delete(T entity) throws Exception;
Collection<T> readAll(int start, int size) throws Exception;
long count() throws Exception;
}
Define the CRUD sub-interface, one for each Entity. Notice that those interfaces can add business operations to the inherited CRUD ones, as suggested in the comments.
@Remote
public interface FpUserFacade extends
FootprintEntityFacade<FpUser> {
// void changeRating(FpEvent event, int newRating);
}
@Remote
public interface FpCertificateFacade extends
FpEntityFacade<FpCertificate> {
}
Now we implement our CRUD interface. Here, the only trick is the
reflection used in the constructor to catch the class of the entity;
this avoids the need to include the type of the entity every time we
construct a new facade. Also observe the EntityManager injected in the
bean, which characterizes those beans more as Entity Manager
Wrapper than DAO, actually.
import java.io.Serializable;
import java.util.*;
import javax.ejb.Stateless;
import javax.persistence.*;
import net.java.dev.footprint.service.entity.*;
@Stateless
public abstract class
CRUDEntityFacade<T extends AbstractFootprintEntity>
implements FpEntityFacade<T> {
private transient final Class<T> entityClass;
@PersistenceContext(name = "footprint")
protected transient EntityManager manager;
@SuppressWarnings("unchecked")
public CRUDEntityFacade() {
entityClass = (Class<T>)
((java.lang.reflect.ParameterizedType)
this.getClass().getGenericSuperclass())
.getActualTypeArguments()[0];
}
public T create(final T entity) throws Exception {
manager.merge(entity);
manager.flush();
return entity;
}
public Collection<T> readAll(int offset, int limit)
throws Exception {
if (offset < 0) { offset = 0; }
if (limit <= 0 || limit > 50) { limit = 50; }
Query query;
query = manager.createQuery("select e from "
+ entityClass.getSimpleName() + " e");
query.setFirstResult(offset);
query.setMaxResults(offset + limit);
return doQuery(query);
}
public T read(final Serializable primaryKey)
throws Exception {
return manager.find(entityClass, primaryKey);
}
public void delete(final T entity) throws Exception {
manager.remove(entity);
manager.flush();
}
public T update(final T entity) throws Exception {
return manager.merge(entity);
}
public long count() throws Exception {
Query query =
manager.createQuery("select count(e.id) from "
+ entityClass.getSimpleName() + " e");
Number countResult = (Number) query.getSingleResult();
return countResult.longValue();
}
}
As you notice in the above code, the update method is an important
illustration of exposing domain models through the service interface. All
objects transfered between the client and the service are JPA Detached
Objects, so you can merge the modified data back into the database only if the version of the detached object is the same as the version number
stored in the database. That will not work in two cases:
FpUser entities back in the database, it will
work, but it will delete the relationship between FpUser and
FpCertificate, inserting null into the relationship column.So you will be forced to load the original entity, copy programatically the new data from the incoming object to the original entity object, and finally call the merge operation of JPA. It is an unavoidable procedure, and the general approach for doing that is to use reflection-based APIs like the commons beanutils.
Next, implement the CRUD sub-interfaces. It is important to notice
here the safe update method overriding the generic one. The same should
be done to the FpCertificate and the other entities. Another important
aspect is that we exclude the relationship from this safe copy. It
works if your detached entity comes with complete and valid
information from outside the service interface, but for security
reasons it is better to update the relationship between entities only
through specialized business methods. Imagine, for example, if someone
starts to manipulate the entities relationship outside the service
interface and your facade just inserts it back into the database without
filters; it's just too dangerous. Nevertheless, it is important to state
that a merge of relationships works out of the box, and eventually it
can be useful if the client is another service in the same security
zone.
A special remark about the delete operation in the parent-child
relationship between FpCertificate and FpUser: deletion of an
FpCertificate entity (child) requires no extra code, but
deletion of an FpUser entity (parent) requires a bulk operation,
because JPA does not propagate the deletion of a parent to its children.
You need to delete the foreign key references to an FpUser entity
before deleting it; otherwise, the bulk operation will be rolled back.
[Special note: if you apply Hibernate, you can use CascadeType.DELETE_ORPHAN;
using TopLink, you have the option to configure the relationships
as private-owned.
I didn't apply such features because they are both proprietary, but I
expect such functionality to be included in a next EJB/JPA
specification.]
@Stateless
public class UserFacade extends CRUDEntityFacade<FpUser>
implements FpUserFacade {
@Override
public FpUser update(FpUser entity) throws Exception {
FpUser attached =
manager.find(FpUser.class, entity.getId());
// this can be done with Commons BeanUtils
attached.setEmail(entity.getEmail());
attached.setName(entity.getName());
return manager.merge(attached);
}
@Override
public void delete(long id) throws IllegalStateException,
IllegalArgumentException, TransactionRequiredException,
PersistenceException {
Query query = manager.
createNamedQuery(FpCertificate.BULK_DELETE_CERTIFICATES);
query.setParameter(FpCertificate.USER_ID_PARAM, id);
query.executeUpdate();
FpUser user = manager.find(FpUser.class, new Long(id));
manager.remove(user);
}
}
public class CertificateFacade
extends CRUDEntityFacade<FpCertificate>
implements FpCertificateFacade {
@Override
public FpCertificate update(FpCertificate entity)
throws Exception {
FpCertificate attached =
manager.find(FpCertificate.class, entity.getId());
attached.setPath(entity.getPath());
return manager.merge(attached);
}
}
Our domain model is ready to be exposed on the web. Below you
find an example of a Jersey annotated resource that exposes the FpUser
entities in their service end points (logging and exception handling were
removed for the sake of clarity). Observe how few lines of code are required to
traverse the data directly from the database to the service endpoint
without any copy (unless for the update method explained above). Another
important aspect is the security of the CRUD operations: it is
the responsibility on the service implementation to check if the client or
the authenticated user is allowed to access the referred entities; here
I am just showing the raw code, but in a production environment one should include
business validation on the service interface.
import java.util.*;
import javax.ejb.EJB;
import javax.ws.rs.*;
import javax.xml.bind.*;
import net.java.dev.footprint.service.entity.*;
import static javax.ws.rs.core.MediaType.*;
@Path("/user")
public class UserResource {
@EJB
private FpUserFacade userFacade;
@Produces( { APPLICATION_XML, APPLICATION_JSON })
@Consumes( { APPLICATION_XML, APPLICATION_JSON })
@PUT
@Path("/create")
public FpUser create(FpUser newUser) {
return FpUser user = userFacade.create(newUser);
}
@GET
@Produces( { APPLICATION_XML, APPLICATION_JSON })
@Path("/read/{id}")
public FpUser read(@PathParam("id") String id) {
userFacade.read(new Long(id)));
}
@Produces( { APPLICATION_XML, APPLICATION_JSON })
@Consumes( { APPLICATION_XML, APPLICATION_JSON })
@PUT
@Path("/update")
public FpUser update(FpUser user) {
return userFacade.update(user);
}
@DELETE
@Path("/delete/{id}")
public void delete(@PathParam("id") String id) {
FpUser user = userFacade.read(id);
userFacade.delete(user);
}
@GET
@Produces( { APPLICATION_XML, APPLICATION_JSON })
@Path("/editall/{offset}/{limit}")
public Collection<FpUser> editAll(
@PathParam("offset") int offset
, @PathParam("limit") int limit) {
return userFacade.readAll(offset, limit);
}
}
Observe the nice Jersey feature of exposing the
data in two different formats: XML and JSON. Note that the client should
include the HTTP header Accept:application/json to receive the data in
JSON format.
The code presented in this article is a snapshot of the Footprint Service Project. In order to install and test the sample project, do the following:
asadmin start-database
asadmin start-domain domain1
asadmin deploy --user admin --password adminadmin
footprint-service-ear-1.0-SNAPSHOT.ear
Once you've done this, you can test the application opening the URLs below in a
web browser. To invoke the POST, PUT, and DELETE methods, you can use the
CURL tool on UNIX-based systems. If you are using Windows, you can use CURL
for Windows. Remember: if you set the HTTP Accept header to application/json,
the same code will respond in JSON format.
curl -H "Accept:application/json" -X GET
http://localhost:8080/footprint-service/mock/create
curl -H "Accept:application/json" -X GET
http://localhost:8080/footprint-service/user/editall/0/99
http://localhost:8080/footprint-service/certificate/readall/0/99
curl -H "Accept:application/json" -X GET
http://localhost:8080/footprint-service/certificate/readall/0/99
Create a new user. Below you find the content of a sample test.xml file. Notice the ID element is empty, to avoid causing a conflict in the database.
curl -v -H "Content-Type: application/xml" -X
PUT --data-binary @test.xml
http://localhost:8080/footprint-service/user/create
<?xml version="1.0" encoding="UTF-8"
standalone="yes"?> <ns2:fpUser
xmlns:ns2="http://footprint.dev.java.net/service/entity"
version="1"><id></id><name>Your
Name</name><email>email@test.com</email><organization
version="1"><id>3</id><logotype>http://your.company/logo.gif</logotype><name>Your
Company</name><website>http://website.com</website></organization></ns2:fpUser>
Update an existing user. You can use the same test.xml file, but remember to include the ID of the entity that you want to update, and also to modify another field so you can see the changes.
curl -v -H "Content-Type: application/xml" -X
POST --data-binary @test.xml
http://fgaucho.dyndns.org:8080/footprint-service/user/update
Delete a user. Include an existing ID in the URL. Be aware that the only sign of success is the response HTTP code 204.
curl -v -H "Accept:application/json" -X
DELETE http://fgaucho.dyndns.org:8080/footprint-service/user/delete/2
The sample project in this article has no pagination, security, or other advanced
features, but if you want more complete code, you can check out the
Footprint Service from the Subversion
repository and then compile it through Maven as shown below. When
svn asks for a password, just press ENTER.
svn checkout https://footprint.dev.java.net/svn/footprint/trunk footprint --username guest
cd footprint
mvn clean install
You will find the packed EAR file in the target folder. Questions about the service design and how to install it can be posted directly to the Footprint project's dev mailing list.
JAXB and JPA can be combined to reduce the boilerplate code of Java EE applications and also to optimize the performance of RESTful web services--a flexible solution, which preserves the original domain model while following the JPA, JAXB, and HTTP standards. Preliminary tests proved that this solution surpasses the performance of traditional techniques based on adapters; nevertheless, our job is not yet complete. Exposing the full domain model through a web service interface is not realistic when you consider production environments. In the second part of this series, I will demonstrate how to control the exposure of the domain model in different service paths, allowing fine-grained management of which parts of the entity beans will be exposed to which group of the application users, providing insight into how to produce robust and scalable RESTful web services based on Java EE technologies.
Several people contributed ideas included in this article: the Footprint project members, friends, and anonymous tips in diverse mailing lists, especially the Jersey and Glassfish mailing lists. I also would like to register special thanks to Roland Huecking and Rudolf Fluetsch for inspirational discussions about Java EE technologies.
|
UML to JAXB / JPA model
- xml schemas (JAXB 2.1)
- java model (JPA 1.0 ie eclipseLink 1.1.1)
- SQL scripts
- HTML model documentation
- test code
- web Model browser with an XML validator and upload document to database
This open source project is available : VO-URP Home