JBoss.orgCommunity Documentation

Chapter 8. Errai JPA

8.1. Getting Started
8.1.1. META-INF/persistence.xml
8.1.2. Declaring an Entity Class
8.1.3. Entity Lifecycle States
8.1.4. Obtaining an instance of EntityManager
8.1.5. Named Queries
8.1.6. Entity Lifecycle Events
8.1.7. JPA Metamodel
8.1.8. JPA Features Not Implemented in Errai 2.4
8.1.9. Other Caveats for Errai 2.1 JPA
8.2. Errai JPA Data Sync
8.2.1. How To Use It

Deprecated

Errai JPA is deprecated. These features are no longer being updated and will be removed in a future release.

Starting with Errai 2.1, Errai implements a subset of JPA 2.0. With Errai JPA, you can store and retrieve entity objects on the client side, in the browser’s local storage. This allows the reuse of JPA-related code (both entity class definitions and procedural logic that uses the EntityManager) between client and server.

Errai JPA implements the following subset of JPA 2.0:

It’s all client-side

Errai JPA is a declarative, typesafe interface to the web browser’s localStorage object. As such it is a client-side implementation of JPA. Objects are stored and fetched from the browser’s local storage, not from the JPA provider on the server side.

Manual Setup

Checkout the Manual Setup Section for instructions on how to manually add Errai JPA to your project.

Classes whose instances can be stored and retrieved by JPA are called entities. To declare a class as a JPA entity, annotate it with @Entity.

JPA requires that entity classes conform to a set of rules. These are:

Here is an example of a valid entity class with an ID attribute (id) and a String-valued persistent attribute (name):

@Entity

public class Genre {
  @Id @GeneratedValue
  private int id;
  private String name;
  // This constructor is used by JPA
  public Genre() {}
  // This constructor is not used by JPA
  public Genre(String name) {
    this();
    this.name = name;
  }
  // These getter and Setter methods are optional:
  public int getId() { return id; }
  public void setId(int id) { this.id = id; }
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
}

When an entity changes state (more on this later), that state change can be cascaded automatically to related entity instances. By default, no state changes are cascaded to related entities. To request cascading of entity state changes, use the cascade attribute on any of the relationship quantifiers @OneToOne, @ManyToOne, @OneToMany, and @ManyToMany.

CascadeType valueDescription

PERSIST

Persist the related entity object(s) when this entity is persisted

MERGE

Merge the attributes of the related entity object(s) when this entity is merged

REMOVE

Remove the related entity object(s) from persistent storage when this one is removed

REFRESH

Not applicable in Errai JPA

DETACH

Detach the related entity object(s) from the entity manager when this object is detached

ALL

Equivalent to specifying all of the above

For an example of specifying cascade rules, refer to the Artist example above. In that example, the cascade type on albums is ALL. When a particular Artist is persisted or removed, detached, etc., all of that artist’s albums will also be persisted or removed, or detached correspondingly. However, the cascade rules for genres are different: we only specify PERSIST and MERGE. Because a Genre instance is reusable and potentially shared between many artists, we do not want to remove or detach these when one artist that references them is removed or detached. However, we still want the convenience of automatic cascading persistence in case we persist an Artist which references a new, unmanaged Genre.

The entity manager provides the means for storing, retrieving, removing, and otherwise affecting the lifecycle state of entity instances.

To obtain an instance of EntityManager on the client side, use Errai IoC (or CDI) to inject it into any client-side bean:

@EntryPoint

public class Main {
  @Inject EntityManager em;
}

To retrieve one or more entities that match a set of criteria, Errai JPA allows the use of JPA named queries. Named queries are declared in annotations on entity classes.

To receive a notification when an entity instance transitions from one lifecycle state to another, use an entity lifecycle listener.

These annotations can be applied to methods in order to receive notifications at certain points in an entity’s lifecycle. These events are delivered for direct operations initiated on the EntityManager as well as operations that happen due to cascade rules.

AnnotationMeaning

@PrePersist

The entity is about to be persisted or merged into the entity manager.

@PostPersist

The entity has just been persisted or merged into the entity manager.

@PreUpdate

The entity’s state is about to be captured into the browser’s localStorage.

@PostUpdate

The entity’s state has just been captured into the browser’s localStorage.

@PreRemove

The entity is about to be removed from persistent storage.

@PostRemove

The entity has just been removed from persistent storage.

@PostLoad

The entity’s state has just been retrieved from the browser’s localStorage.

JPA lifecycle event annotations can be placed on methods in the entity type itself, or on a method of any type with a public no-args constructor.

To receive lifecycle event notifications directly on the affected entity instance, create a no-args method on the entity class and annotate it with one or more of the lifecycle annotations in the above table.

For example, here is a variant of the Album class where instances receive notification right after they are loaded from persistent storage:

@Entity

public class Album {
  ... same as before ...
  @PostLoad
  public void postLoad() {
    System.out.println("Album " + getName() + " was just loaded into the entity manager");
  }
}

To receive lifecycle methods in a different class, declare a method that takes one parameter of the entity type and annotate it with the desired lifecycle annotations. Then name that class in the @EntityListeners annotation on the entity type.

The following example produces the same results as the previous example:

@Entity

@EntityListeners(StandaloneLifecycleListener.class)
public class Album {
  ... same as always ...
}
public class StandaloneLifecycleListener {
  @PostLoad
  public void albumLoaded(Album a) {
  public void postLoad() {
    System.out.println("Album " + a.getName() + " was just loaded into the entity manager");
  }
}

Traditional JPA implementations allow you to store and retrieve entity objects on the server side. Errai’s JPA implementation allows you to store and retrieve entity objects in the web browser using the same APIs. All that’s missing is the ability to synchronize the stored data between the server side and the client side.

This is where Errai JPA Data Sync comes in: it provides an easy mechanism for two-way synchronization of data sets between the client and the server.

Manual Setup

Checkout the Manual Setup Section for instructions on how to manually add Errai JPA Datasync to your project.

For the rest of this chapter, we will refer to the following Entity classes, which are defined in a shared package that’s visible to client and server code:

@Portable

@Entity
@NamedQuery(name = "allUsers", query = "SELECT u FROM User u")
public class User {
  @Id
  @GeneratedValue
  private long id;
  private String name;
  // getters and setters omitted
}
@Portable

@Entity
@NamedQuery(name = "groceryListsForUser", query = "SELECT gl FROM GroceryList gl WHERE gl.owner=:user")
public class GroceryList {
  @Id
  @GeneratedValue
  private long id;
  @ManyToOne
  private User owner;
  @OneToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH })
  private List<Item> items = new ArrayList<Item>();
  // getters and setters omitted
}
@Portable

@Entity
@NamedQuery(name = "allItems", query = "SELECT i FROM Item i")
public class Item {
  @Id
  @GeneratedValue
  private long id;
  private String name;
  private String department;
  private String comment;
  private Date addedOn;
  @ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH })
  private User addedBy;
  // getters and setters omitted
}

To summarize: there are three entity types: User, GroceryList, and Item. Each GroceryList belongs to a User and has a list of Item objects.

Now let’s say we want to synchronize the data for all of a user’s grocery lists. This will make them available for offline use through Errai JPA, and at the same time it will update the server with the latest changes made on the client. Ultimately, the sync operation is accomplished via an annotated method or an asynchronous call into ClientSyncManager, but first we have to prepare a few things on the client and the server.

[[sid-71467090_ErraiJPADataSync-ServerSide%E2%80%93DataSyncServiceImpl]]

A data sync operation begins when the client-side sync manager sends an Errai RPC request to the server. Although a server-side implementation of the remote interface is provided, you are responsible for implementing a thin wrapper around it. This wrapper serves two purposes:

  1. It allows you to determine how to obtain a reference to the JPA EntityManager (and to choose which persistence context the server-side data sync will operate on)
  2. It allows you to inspect the contents of each sync request and make security decisions about access to particular entities

If you are deploying to a container that supports CDI and EJB 3, you can use this DataSyncServiceImpl as a template for your own:

@Stateless @org.jboss.errai.bus.server.annotations.Service

public class DataSyncServiceImpl implements DataSyncService {
  @PersistenceContext
  private EntityManager em;
  private final JpaAttributeAccessor attributeAccessor = new JavaReflectionAttributeAccessor();
  @Inject private LoginService loginService;
  @Override
  public <X> List<SyncResponse<X>> coldSync(SyncableDataSet<X> dataSet, List<SyncRequestOperation<X>> remoteResults) {
    // Ensure a user is logged in
    User currentUser = loginService.whoAmI();
    if (currentUser == null) {
      throw new IllegalStateException("Nobody is logged in!");
    }
    // Ensure user is accessing their own data!
    if (dataSet.getQueryName().equals("groceryListsForUser")) {
      User requestedUser = (User) dataSet.getParameters().get("user");
      if (!currentUser.getId().equals(requestedUser.getId())) {
        throw new AccessDeniedException("You don't have permission to sync user " + requestedUser.getId());
      }
    }
    else {
      throw new IllegalArgumentException("You don't have permission to sync dataset " + dataSet.getQueryName());
    }
    DataSyncService dss = new org.jboss.errai.jpa.sync.server.DataSyncServiceImpl(em, attributeAccessor);
    return dss.coldSync(dataSet, remoteResults);
  }
}

If you are not using EJB 3, you will not be able to use the @PersistenceContext annotation. In this case, obtain a reference to your EntityManager the same way you would anywhere else in your application.

Like many Errai features, Errai JPA DataSync provides an annotation-driven programming model and a programmatic API. You can choose which to use based on your needs and preferences.

The declarative data sync API is driven by the @Sync annotation. Consider the following example client-side class:

  // This injected User object could have been set up in a @PostConstruct method instead of being injected.

  @Inject
  private User syncThisUser;
  @Sync(query = "groceryListsForUser", params = { @SyncParam(name = "user", val = "{syncThisUser}") })
  private void onDataSyncComplete(SyncResponses<GroceryList> responses) {
    Window.alert("Data Sync Complete!");
  }

By placing the above code snippet in a client-side bean, you tell Errai JPA Data Sync that, as long as a managed instance of the bean containing the @Sync method exists, the Data Sync system should keep all grocery lists belonging to the syncThisUser user in sync between the client-side JPA EntityManager and the server-side EntityManager. Right now, the data sets are kept in sync using a sync request every 5 seconds. In the future, this may be optimised to an incremental approach that pushes changes as they occur.

The annotated method needs to have exactly one parameter of type SyncResponses and will be called each time a data sync operation has completed. All sync operations passed to the method will have already been applied to the local EntityManager, with conflicts resolved in favour of the server’s version of the data. The original client values are available in the SyncResponses object, which gives you a chance to implement a different conflict resolution policy.

The query attribute on the @Sync annotation must refer to an existing JPA Named Query that is defined on a shared JPA entity class.

The params attribute is an array of @SyncParam annotations. There must be exactly one @SyncParam for each named parameter in the JPA query (positional parameters are not supported). If the val argument is surrounded with brace brackets (as it is in the example aboce) then it is interpreted as a reference to a declared or inherited field in the containing class. Otherwise, it is interpreted as a literal String value.

Transport (network) errors are logged to the slf4j logger channel org.jboss.errai.jpa.sync.client.local.ClientSyncWorker. As of Errai 3.0.0.M4, it is not possible to specify a custom error handler using the declarative API. See the next section for information about the programmatic API.

When the client sends the sync request to the server, it includes information about the state it expects each entity to be in. If an entity’s state on the server does not match this expected state on the client, the server ignores the client’s change request and includes a ConflictResponse object in the sync reply.

When the client processes the sync responses from the server, it applies the new state from the server to the local data store. This overwrites the change that was initially requested from the client. In short, you could call this the "server wins" conflict resolution policy.

In some cases, your application may be able to do something smarter: apply domain-specific knowledge to merge the conflict automatically, or prompt the user to perform a manual merge. In order to do this, you will have to examine the server response from inside the onCompletion callback you provided to the coldSync() method:

    RemoteCallback<List<SyncResponse<GroceryList>>> onCompletion = new RemoteCallback<List<SyncResponse<GroceryList>>>() {

      @Override
      public void callback(List<SyncResponse<GroceryList>> responses) {
        for (SyncResponse<GroceryList> response : responses) {
          if (response instanceof ConflictResponse) {
            ConflictResponse<GroceryList> cr = (ConflictResponse<GroceryList>) response;
            List<Item> expectedItems = cr.getExpected().getItems();
            List<Item> serverItems = cr.getActualNew().getItems();
            List<Item> clientItems = cr.getRequestedNew().getItems();
            // merge the list of items by comparing each to expectedItems
            List<Item> merged = ...;
            // update local storage with the merged list
            em.find(GroceryList.class, cr.getActualNew().getId()).setItems(merged);
            em.flush();
          }
        }
      }
    };

Remember, because of Errai’s default "server wins" resolution policy, the call to em.find(GroceryList.class, cr.getActualNew().getId()) will return a GroceryList object that has already been updated to match the state present in serverItems.