--- /dev/null
+[[adding-jpa-to-the-address-book-demo]]
+Adding JPA to the address book demo
+-----------------------------------
+
+Petter Holmström
+
+[[introduction]]
+Introduction
+~~~~~~~~~~~~
+
+The https://github.com/vaadin/addressbook/tree/v7[Vaading address book] tutorial (the one
+hour version, that is) does a very good job introducing the different
+parts of Vaadin. However, it only uses an in-memory data source with
+randomly generated data. This may be sufficient for demonstration
+purposes, but not for any real world applications that manage data.
+Therefore, in this article, we are going to replace the tutorial's
+in-memory data source with the Java Persistence API (JPA) and also
+utilize some of the new JEE 6 features of
+https://glassfish.dev.java.net/[GlassFish] 3.
+
+[[prerequisites]]
+Prerequisites
+^^^^^^^^^^^^^
+
+In order to fully understand this article, you should be familiar with
+JEE and JPA development and you should also have read through the Vaadin
+tutorial.
+
+If you want to try out the code in this article you should get the
+latest version of GlassFish 3 (build 67 was used for this article) and
+http://ant.apache.org[Apache Ant 1.7]. You also need to download the
+https://github.com/eriklumme/doc-attachments/blob/master/attachments/addressbook.tar.gz[source code]. *Note, that you have to edit the
+_build.xml_ file to point to the correct location of the GlassFish
+installation directory before you can use it!*
+
+[[the-system-architecture]]
+The System Architecture
+~~~~~~~~~~~~~~~~~~~~~~~
+
+The architecture of the application is presented in the following
+diagram:
+
+image:img/architecture2.png[System architecture diagram]
+
+In addition to the Vaadin UI created in the tutorial, we will add a
+stateless Enterprise Java Bean (EJB) to act as a facade to the database.
+The EJB will in turn use JPA to communicate with a JDBC data source (in
+this example, the built-in `jdbc/sample` data source).
+
+[[refactoring-the-domain-model]]
+Refactoring the Domain Model
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Before doing anything else, we have to modify the domain model of the
+Address Book example.
+
+[[the-person-class]]
+The Person class
+^^^^^^^^^^^^^^^^
+
+In order to use JPA, we have to add JPA annotations to the `Person`
+class:
+
+[source,java]
+....
+// Imports omitted
+@Entity
+public class Person implements Serializable {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+ @Version
+ @Column(name = "OPTLOCK")
+ private Long version;
+ private String firstName = "";
+ private String lastName = "";
+ private String email = "";
+ private String phoneNumber = "";
+ private String streetAddress = "";
+ private Integer postalCode = null;
+ private String city = "";
+
+ public Long getId() {
+ return id;
+ }
+
+ public Long getVersion() {
+ return version;
+ }
+ // The rest of the methods omitted
+}
+....
+
+As we do not need to fit the domain model onto an existing database, the
+annotations become very simple. We have only marked the class as being
+an entity and added an ID and a version field.
+
+[[the-personreference-class]]
+The PersonReference class
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+There are many advantages with using JPA or any other Object Persistence
+Framework (OPF). The underlying database gets completely abstracted away
+and we can work with the domain objects themselves instead of query
+results and records. We can detach domain objects, send them to a client
+using a remote invocation protocol, then reattach them again.
+
+However, there are a few use cases where using an OPF is not such a good
+idea: reporting and listing. When a report is generated or a list of
+entities is presented to the user, normally only a small part of the
+data is actually required. When the number of objects to fetch is large
+and the domain model is complex, constructing the object graphs from the
+database can be a very lengthy process that puts the users' patience to
+the test – especially if they are only trying to select a person's name
+from a list.
+
+Many OPFs support lazy loading of some form, where references and
+collections are fetched on demand. However, this rarely works outside
+the container, e.g. on the other side of a remoting connection.
+
+One way of working around this problem is to let reports and lists
+access the database directly using SQL. This is a fast approach, but it
+also couples the code to a particular SQL dialect and therefore to a
+particular database vendor.
+
+In this article, we are going to select the road in the middle – we will
+only fetch the property values we need instead of the entire object, but
+we will use PQL and JPA to do so. In this example, this is a slight
+overkill as we have a very simple domain model. However, we do this for
+two reasons: Firstly, as Vaadin is used extensively in business
+applications where the domain models are complex, we want to introduce
+this pattern in an early stage. Secondly, it makes it easier to plug
+into Vaadin's data model.
+
+In order to implement this pattern, we need to introduce a new class,
+namely `PersonReference`:
+
+[source,java]
+....
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.ObjectProperty;
+// Some imports omitted
+
+public class PersonReference implements Serializable, Item {
+ private Long personId;
+ private Map<Object, Property> propertyMap;
+
+ public PersonReference(Long personId, Map<String, Object> propertyMap) {
+ this.personId = personId;
+ this.propertyMap = new HashMap<Object, Property>();
+ for (Map.Entry<Object, Property> entry : propertyMap.entrySet()) {
+ this.propertyMap.put(entry.getKey(), new ObjectProperty(entry.getValue()));
+ }
+ }
+
+ public Long getPersonId() {
+ return personId;
+ }
+
+ public Property getItemProperty(Object id) {
+ return propertyMap.get(id);
+ }
+
+ public Collection<?> getItemPropertyIds() {
+ return Collections.unmodifiableSet(propertyMap.keySet());
+ }
+
+ public boolean addItemProperty(Object id, Property property) {
+ throw new UnsupportedOperationException("Item is read-only.");
+ }
+
+ public boolean removeItemProperty(Object id) {
+ throw new UnsupportedOperationException("Item is read-only.");
+ }
+}
+....
+
+The class contains the ID of the actual `Person` object and a `Map` of
+property values. It also implements the `com.vaadin.data.Item`
+interface, which makes it directly usable in Vaadin's data containers.
+
+[[the-querymetadata-class]]
+The QueryMetaData class
+^^^^^^^^^^^^^^^^^^^^^^^
+
+Before moving on to the EJB, we have to introduce yet another class,
+namely `QueryMetaData`:
+
+[source,java]
+....
+// Imports omitted
+public class QueryMetaData implements Serializable {
+
+ private boolean[] ascending;
+ private String[] orderBy;
+ private String searchTerm;
+ private String propertyName;
+
+ public QueryMetaData(String propertyName, String searchTerm, String[] orderBy, boolean[] ascending) {
+ this.propertyName = propertyName;
+ this.searchTerm = searchTerm;
+ this.ascending = ascending;
+ this.orderBy = orderBy;
+ }
+
+ public QueryMetaData(String[] orderBy, boolean[] ascending) {
+ this(null, null, orderBy, ascending);
+ }
+
+ public boolean[] getAscending() {
+ return ascending;
+ }
+
+ public String[] getOrderBy() {
+ return orderBy;
+ }
+
+ public String getSearchTerm() {
+ return searchTerm;
+ }
+
+ public String getPropertyName() {
+ return propertyName;
+ }
+}
+....
+
+As the class name suggests, this class contains query meta data such as
+ordering and filtering information. We are going to look at how it is
+used in the next section.
+
+[[the-stateless-ejb]]
+The Stateless EJB
+~~~~~~~~~~~~~~~~~
+
+We are now ready to begin designing the EJB. As of JEE 6, an EJB is no
+longer required to have an interface. However, as it is a good idea to
+use interfaces at the boundaries of system components, we will create
+one nonetheless:
+
+[source,java]
+....
+// Imports omitted
+@TransactionAttribute
+@Local
+public interface PersonManager {
+
+ public List<PersonReference> getPersonReferences(QueryMetaData queryMetaData, String... propertyNames);
+
+ public Person getPerson(Long id);
+
+ public Person savePerson(Person person);
+}
+....
+
+Please note the `@TransactionAttribute` and `@Local` annotations that
+instruct GlassFish to use container managed transaction handling, and to
+use local references, respectively. Next, we create the implementation:
+
+[source,java]
+....
+// Imports omitted
+@Stateless
+public class PersonManagerBean implements PersonManager {
+
+ @PersistenceContext
+ protected EntityManager entityManager;
+
+ public Person getPerson(Long id) {
+ // Implementation omitted
+ }
+
+ public List<PersonReference> getPersonReferences(QueryMetaData queryMetaData, String... propertyNames) {
+ // Implementation omitted
+ }
+
+ public Person savePerson(Person person) {
+ // Implementation omitted
+ }
+}
+....
+
+We use the `@Stateless` annotation to mark the implementation as a
+stateless session EJB. We also use the `@PersistenceContext` annotation
+to instruct the container to automatically inject the entity manager
+dependency. Thus, we do not have to do any lookups using e.g. JNDI.
+
+Now we can move on to the method implementations.
+
+[source,java]
+....
+public Person getPerson(Long id) {
+ return entityManager.find(Person.class, id);
+}
+....
+
+This implementation is very straight-forward: given the unique ID, we
+ask the entity manager to look up the corresponding `Person` instance
+and return it. If no such instance is found, `null` is returned.
+
+[source,java]
+....
+public List<PersonReference> getPersonReferences(QueryMetaData queryMetaData, String... propertyNames) {
+ StringBuffer pqlBuf = new StringBuffer();
+ pqlBuf.append("SELECT p.id");
+ for (int i = 0; i < propertyNames.length; i++) {
+ pqlBuf.append(",");
+ pqlBuf.append("p.");
+ pqlBuf.append(propertyNames[i]);
+ }
+ pqlBuf.append(" FROM Person p");
+
+ if (queryMetaData.getPropertyName() != null) {
+ pqlBuf.append(" WHERE p.");
+ pqlBuf.append(queryMetaData.getPropertyName());
+ if (queryMetaData.getSearchTerm() == null) {
+ pqlBuf.append(" IS NULL");
+ } else {
+ pqlBuf.append(" = :searchTerm");
+ }
+ }
+
+ if (queryMetaData != null && queryMetaData.getAscending().length > 0) {
+ pqlBuf.append(" ORDER BY ");
+ for (int i = 0; i < queryMetaData.getAscending().length; i++) {
+ if (i > 0) {
+ pqlBuf.append(",");
+ }
+ pqlBuf.append("p.");
+ pqlBuf.append(queryMetaData.getOrderBy()[i]);
+ if (!queryMetaData.getAscending()[i]) {
+ pqlBuf.append(" DESC");
+ }
+ }
+ }
+
+ String pql = pqlBuf.toString();
+ Query query = entityManager.createQuery(pql);
+ if (queryMetaData.getPropertyName() != null && queryMetaData.getSearchTerm() != null) {
+ query.setParameter("searchTerm", queryMetaData.getSearchTerm());
+ }
+
+ List<Object[]> result = query.getResultList();
+ List<PersonReference> referenceList = new ArrayList<PersonReference>(result.size());
+
+ HashMap<String, Object> valueMap;
+ for (Object[] row : result) {
+ valueMap = new HashMap<String, Object>();
+ for (int i = 1; i < row.length; i++) {
+ valueMap.put(propertyNames[i - 1], row[i]);
+ }
+ referenceList.add(new PersonReference((Long) row[0], valueMap));
+ }
+ return referenceList;
+}
+....
+
+This method is a little more complicated and also demonstrates the usage
+of the `QueryMetaData` class. What this method does is that it
+constructs a PQL query that fetches the values of the properties
+provided in the `propertyNames` array from the database. It then uses
+the `QueryMetaData` instance to add information about ordering and
+filtering. Finally, it executes the query and returns the result as a
+list of `PersonReference` instances.
+
+The advantage with using `QueryMetaData` is that additional query
+options can be added without having to change the interface. We could
+e.g. create a subclass named `AdvancedQueryMetaData` with information
+about wildcards, result size limitations, etc.
+
+[source,java]
+....
+public Person savePerson(Person person) {
+ if (person.getId() == null)
+ entityManager.persist(person);
+ else
+ entityManager.merge(person);
+ return person;
+}
+....
+
+This method checks if `person` is persistent or transient, merges or
+persists it, respectively, and finally returns it. The reason why
+`person` is returned is that this makes the method usable for remote
+method calls. However, as this example does not need any remoting, we
+are not going to discuss this matter any further in this article.
+
+[[plugging-into-the-ui]]
+Plugging Into the UI
+~~~~~~~~~~~~~~~~~~~~
+
+The persistence component of our Address Book application is now
+completed. Now we just have to plug it into the existing user interface
+component. In this article, we are only going to look at some of the
+changes that have to be made to the code. That is, if you try to deploy
+the application with the changes presented in this article only, it will
+not work. For all the changes, please check the source code archive
+attached to this article.
+
+[[creating-a-new-container]]
+Creating a New Container
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+First of all, we have to create a Vaadin container that knows how to
+read data from a `PersonManager`:
+
+[source,java]
+....
+// Imports omitted
+public class PersonReferenceContainer implements Container, Container.ItemSetChangeNotifier {
+
+ public static final Object[] NATURAL_COL_ORDER = new String[] {"firstName", "lastName", "email",
+ "phoneNumber", "streetAddress", "postalCode", "city"};
+ protected static final Collection<Object> NATURAL_COL_ORDER_COLL = Collections.unmodifiableList(
+ Arrays.asList(NATURAL_COL_ORDER)
+ );
+ protected final PersonManager personManager;
+ protected List<PersonReference> personReferences;
+ protected Map<Object, PersonReference> idIndex;
+ public static QueryMetaData defaultQueryMetaData = new QueryMetaData(
+ new String[]{"firstName", "lastName"}, new boolean[]{true, true});
+ protected QueryMetaData queryMetaData = defaultQueryMetaData;
+ // Some fields omitted
+
+ public PersonReferenceContainer(PersonManager personManager) {
+ this.personManager = personManager;
+ }
+
+ public void refresh() {
+ refresh(queryMetaData);
+ }
+
+ public void refresh(QueryMetaData queryMetaData) {
+ this.queryMetaData = queryMetaData;
+ personReferences = personManager.getPersonReferences(queryMetaData, (String[]) NATURAL_COL_ORDER);
+ idIndex = new HashMap<Object, PersonReference>(personReferences.size());
+ for (PersonReference pf : personReferences) {
+ idIndex.put(pf.getPersonId(), pf);
+ }
+ notifyListeners();
+ }
+
+ public QueryMetaData getQueryMetaData() {
+ return queryMetaData;
+ }
+
+ public void close() {
+ if (personReferences != null) {
+ personReferences.clear();
+ personReferences = null;
+ }
+ }
+
+ public boolean isOpen() {
+ return personReferences != null;
+ }
+
+ public int size() {
+ return personReferences == null ? 0 : personReferences.size();
+ }
+
+ public Item getItem(Object itemId) {
+ return idIndex.get(itemId);
+ }
+
+ public Collection<?> getContainerPropertyIds() {
+ return NATURAL_COL_ORDER_COLL;
+ }
+
+ public Collection<?> getItemIds() {
+ return Collections.unmodifiableSet(idIndex.keySet());
+ }
+
+ public List<PersonReference> getItems() {
+ return Collections.unmodifiableList(personReferences);
+ }
+
+ public Property getContainerProperty(Object itemId, Object propertyId) {
+ Item item = idIndex.get(itemId);
+ if (item != null) {
+ return item.getItemProperty(propertyId);
+ }
+ return null;
+ }
+
+ public Class<?> getType(Object propertyId) {
+ try {
+ PropertyDescriptor pd = new PropertyDescriptor((String) propertyId, Person.class);
+ return pd.getPropertyType();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public boolean containsId(Object itemId) {
+ return idIndex.containsKey(itemId);
+ }
+
+ // Unsupported methods omitted
+ // addListener(..) and removeListener(..) omitted
+
+ protected void notifyListeners() {
+ ArrayList<ItemSetChangeListener> cl = (ArrayList<ItemSetChangeListener>) listeners.clone();
+ ItemSetChangeEvent event = new ItemSetChangeEvent() {
+ public Container getContainer() {
+ return PersonReferenceContainer.this;
+ }
+ };
+
+ for (ItemSetChangeListener listener : cl) {
+ listener.containerItemSetChange(event);
+ }
+ }
+}
+....
+
+Upon creation, this container is empty. When one of the `refresh(..)`
+methods is called, a list of `PersonReference`s are fetched from the
+`PersonManager` and cached locally. Even though the database is updated,
+e.g. by another user, the container contents will not change before the
+next call to `refresh(..)`.
+
+To keep things simple, the container is read only, meaning that all
+methods that are designed to alter the contents of the container throw
+an exception. Sorting, optimization and lazy loading has also been left
+out (if you like, you can try to implement these yourself).
+
+[[modifying-the-personform-class]]
+Modifying the PersonForm class
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+We now have to refactor the code to use our new container, starting with
+the `PersonForm` class. We begin with the part of the constructor that
+creates a list of all the cities currently in the container:
+
+[source,java]
+....
+PersonReferenceContainer ds = app.getDataSource();
+for (PersonReference pf : ds.getItems()) {
+ String city = (String) pf.getItemProperty("city").getValue();
+ cities.addItem(city);
+}
+....
+
+We have changed the code to iterate a collection of `PersonReference`
+instances instead of `Person` instances.
+
+Then, we will continue with the part of the `buttonClick(..)` method
+that saves the contact:
+
+[source,java]
+....
+if (source == save) {
+ if (!isValid()) {
+ return;
+ }
+ commit();
+ person = app.getPersonManager().savePerson(person);
+ setItemDataSource(new BeanItem(person));
+ newContactMode = false;
+ app.getDataSource().refresh();
+ setReadOnly(true);
+}
+....
+
+The code has actually become simpler, as the same method is used to save
+both new and existing contacts. When the contact is saved, the container
+is refreshed so that the new information is displayed in the table.
+
+Finally, we will add a new method, `editContact(..)` for displaying and
+editing existing contacts:
+
+[source,java]
+....
+public void editContact(Person person) {
+ this.person = person;
+ setItemDataSource(new BeanItem(person))
+ newContactMode = false;
+ setReadOnly(true);
+}
+....
+
+This method is almost equal to `addContact()` but uses an existing
+`Person` instance instead of a newly created one. It also makes the form
+read only, as the user is expected to click an Edit button to make the
+form editable.
+
+[[modifying-the-addressbookapplication-class]]
+Modifying the AddressBookApplication class
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Finally, we are going to replace the old container with the new one in
+the main application class. We will start by adding a constructor:
+
+[source,java]
+....
+public AddressBookApplication(PersonManager personManager) {
+ this.personManager = personManager;
+}
+....
+
+This constructor will be used by a custom application servlet to inject
+a reference to the `PersonManager` EJB. When this is done, we move on to
+the `init()` method:
+
+[source,java]
+....
+public void init() {
+ dataSource = new PersonReferenceContainer(personManager);
+ dataSource.refresh(); // Load initial data
+ buildMainLayout();
+ setMainComponent(getListView());
+}
+....
+
+The method creates a container and refreshes it in order to load the
+existing data from the database – otherwise, the user would be presented
+with an empty table upon application startup.
+
+Next, we modify the code that is used to select contacts:
+
+[source,java]
+....
+public void valueChange(ValueChangeEvent event) {
+ Property property = event.getProperty();
+ if (property == personList) {
+ Person person = personManager.getPerson((Long) personList.getValue());
+ personForm.editContact(person);
+ }
+}
+....
+
+The method gets the ID of the currently selected person and uses it to
+lookup the `Person` instance from the database, which is then passed to
+the person form using the newly created `editContact(..)` method.
+
+Next, we modify the code that handles searches:
+
+[source,java]
+....
+public void search(SearchFilter searchFilter) {
+ QueryMetaData qmd = new QueryMetaData((String) searchFilter.getPropertyId(), searchFilter.getTerm(),
+ getDataSource().getQueryMetaData().getOrderBy(),
+ getDataSource().getQueryMetaData().getAscending());
+ getDataSource().refresh(qmd);
+ showListView();
+ // Visual notification omitted
+}
+....
+
+Instead of filtering the container, this method constructs a new
+`QueryMetaData` instance and refreshes the data source. Thus, the search
+operation is performed in the database and not in the container itself.
+
+As we have removed container filtering, we also have to change the code
+that is used to show all contacts:
+
+[source,java]
+....
+public void itemClick(ItemClickEvent event) {
+ if (event.getSource() == tree) {
+ Object itemId = event.getItemId();
+ if (itemId != null) {
+ if (itemId == NavigationTree.SHOW_ALL) {
+ getDataSource().refresh(PersonReferenceContainer.defaultQueryMetaData);
+ showListView();
+ } else if (itemId == NavigationTree.SEARCH) {
+ showSearchView();
+ } else if (itemId instanceof SearchFilter) {
+ search((SearchFilter) itemId);
+ }
+ }
+ }
+}
+....
+
+Instead of removing the filters, this method refreshes the data source
+using the default query meta data.
+
+[[creating-a-custom-servlet]]
+Creating a Custom Servlet
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The original tutorial used an `ApplicationServlet` configured in
+_web.xml_ to start the application. In this version, however, we are
+going to create our own custom servlet. By doing this, we can let
+GlassFish inject the reference to the `PersonManager` EJB using
+annotations, which means that we do not need any JDNI look ups at all.
+As a bonus, we get rid of the _web.xml_ file as well thanks to the new
+JEE 6 `@WebServlet` annotation. The servlet class can be added as an
+inner class to the main application class:
+
+[source,java]
+....
+@WebServlet(urlPatterns = "/*")
+public static class Servlet extends AbstractApplicationServlet {
+
+ @EJB
+ PersonManager personManager;
+
+ @Override
+ protected Application getNewApplication(HttpServletRequest request) throws ServletException {
+ return new AddressBookApplication(personManager);
+ }
+
+ @Override
+ protected Class<? extends Application> getApplicationClass() throws ClassNotFoundException {
+ return AddressBookApplication.class;
+ }
+}
+....
+
+When the servlet is initialized by the web container, the
+`PersonManager` EJB will be automatically injected into the
+`personManager` field thanks to the `@EJB` annotation. This reference
+can then be passed to the main application class in the
+`getNewApplication(..)` method.
+
+[[classical-deployment]]
+Classical Deployment
+~~~~~~~~~~~~~~~~~~~~
+
+Packaging this application into a WAR is no different from the Hello
+World example. We just have to remember to include the _persistence.xml_
+file (we are not going to cover the contents of this file in this
+article), otherwise JPA will not work. Note, that as of JEE 6, we do not
+need to split up the application into a different bundle for the EJB and
+another for the UI. We also do not need any other configuration files
+than the persistence unit configuration file.
+
+The actual packaging can be done using the following Ant target:
+
+[source,xml]
+....
+<target name="package-with-vaadin" depends="compile">
+ <mkdir dir="${dist.dir}"/>
+ <war destfile="${dist.dir}/${ant.project.name}-with-vaadin.war" needxmlfile="false">
+ <lib file="${vaadin.jar}"/>
+ <classes dir="${build.dir}"/>
+ <fileset dir="${web.dir}" includes="**"/>
+ </war>
+</target>
+....
+
+Once the application has been packaged, it can be deployed like so,
+using the *asadmin* tool that comes with GlassFish:
+
+[source,bash]
+....
+$ asadmin deploy /path/to/addressbook-with-vaadin.war
+....
+
+Note, that the Java DB database bundled with GlassFish must be started
+prior to deploying the application. Now we can test the application by
+opening a web browser and navigating to
+http://localhost:8080/addressbook-with-vaadin. The running application
+should look something like this:
+
+image:img/ab-with-vaadin-scrshot.png[Running application screenshot]
+
+[[osgi-deployment-options]]
+OSGi Deployment Options
+~~~~~~~~~~~~~~~~~~~~~~~
+
+The OSGi support of GlassFish 3 introduces some new possibilities for
+Vaadin development. If the Vaadin library is deployed as an OSGi bundle, we can package and
+deploy the address book application without the Vaadin library. The
+following Ant target can be used to create the WAR:
+
+[source,xml]
+....
+<target name="package-without-vaadin" depends="compile">
+ <mkdir dir="${dist.dir}"/>
+ <war destfile="${dist.dir}/${ant.project.name}-without-vaadin.war" needxmlfile="false">
+ <classes dir="${build.dir}"/>
+ <fileset dir="${web.dir}" includes="**"/>
+ </war>
+</target>
+....
+
+[[summary]]
+Summary
+~~~~~~~
+
+In this article, we have extended the Address Book demo to use JPA
+instead of the in-memory container, with an EJB acting as the facade to
+the database. Thanks to annotations, the application does not contain a
+single JNDI lookup, and thanks to JEE 6, the application can be deployed
+as a single WAR.