JBoss Community Archive (Read Only)

Errai

Errai JPA Data Sync

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.

How To Use It

Dependencies

First, ensure your pom.xml includes a dependency on the Data Sync module. This module must be packaged in your application's WAR file, so include it with the default scope (compile):

  <dependency>
    <groupId>org.jboss.errai</groupId>
    <artifactId>errai-jpa-datasync</artifactId>
    <version>${errai.version}</version>
  </dependency>

Then, ensure your project's gwt.xml module descriptor includes a dependency on the Data Sync GWT module:

  <inherits name="org.jboss.errai.jpa.sync.DataSync"/>

A Running Example

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.

All the entities involved in the data synchronization request must be marshallable via Errai Marshalling. This is normally accomplished by adding the @Portable annotation to each JPA entity class, but it is also acceptable to list them in ErraiApp.properties. See the Marshalling section for more details.

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 in one asynchronous call, but first we have to prepare a few things on the client and the server.

Client Side

  @Inject private ClientSyncManager syncManager;
  @Inject private EntityManager em;

  public void syncGroceryLists(User forUser) {
    RemoteCallback<List<SyncResponse<GroceryList>>> onCompletion = new RemoteCallback<List<SyncResponse<GroceryList>>>() {
      @Override
      public void callback(List<SyncResponse<GroceryList>> response) {
        Window.alert("Data Sync Complete!");
      }
    };

    ErrorCallback<?> onError = new BusErrorCallback() {

      @Override
      public boolean error(Message message, Throwable throwable) {
        Window.alert("Data Sync failed!");
        return false;
      }
    };

    Map<String, Object> queryParams = new HashMap<String, Object>();
    queryParams.put("user", forUser);

    syncManager.coldSync("groceryListsForUser", GroceryList.class, queryParams, onCompletion, onError);
  }

The onCompletion and onError callbacks are optional. In the unlikely case that your application doesn't care if a data sync request completed successfully, you can pass null for either callback.

Once your onCompletion callback has been notified, the server and client will have the same entities stored in their respective databases for all entities reachable from the given query result.

Server Side – DataSyncServiceImpl

During the coldSync() call, 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.

Dealing With Conflicts

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.

Searching for ConflictResponse objects in the onCompletion callback is the only way to recover client data that was clobbered in a conflict. If you do not merge this data back into local storage, or at least retain a reference to the cr.getRequestedNew() object, this conflicting client data will be lost forever.

In a future release of Errai JPA, we plan to provide a client-side callback mechanism for custom conflict handling. If such a callback is registered, it will override the default behaviour.

JBoss.org Content Archive (Read Only), exported from JBoss Community Documentation Editor at 2020-03-10 12:34:55 UTC, last content change 2013-08-16 16:12:44 UTC.