SeamFramework.orgCommunity Documentation

Chapter 54. Seam Remoting - Model API

54.1. Introduction
54.2. Model Operations
54.3. Fetching a model
54.3.1. Fetching a bean value
54.4. Modifying model values
54.5. Expanding a model
54.6. Applying Changes

The Model API builds on top of Seam Remoting's object serialization features to provide a component-based approach to working with a server-side object model, as opposed to the RPC-based approach provided by the standard Remoting API. This allows a client-side representation of a server-side object graph to be modified ad hoc by the client, after which the changes made to the objects in the graph can be applied to the corresponding server-side objects. When applying the changes the client determines exactly which objects have been modified by recursively walking the client-side object tree and generating a delta by comparing the original property values of the objects with their new property values.

This approach, when used in conjunction with the extended persistence context provided by Seam elegantly solves a number of problems faced by AJAX developers when working remotely with persistent objects. A persistent, managed object graph can be loaded at the start of a new conversation, and then across multiple requests the client can fetch the objects, make incremental changes to them and apply those changes to the same managed objects after which the transaction can be committed, thereby persisting the changes made.

One other useful feature of the Model API is its ability to expand a model. For example, if you are working with entities with lazy-loaded associations it is usually not a good idea to blindly fetch the associated objects (which may in turn themselves contain associations to other entities, ad nauseum), as you may inadvertently end up fetching the bulk of your database. Seam Remoting already knows how to deal with lazy-loaded associations by automatically excluding them when marshalling instances of entity beans, and assigning them a client-side value of undefined (which is a special JavaScript value, distinct from null). The Model API goes one step further by giving the client the option of manipulating the associated objects also. By providing an expand operation, it allows for the initialization of a previously-uninitialized object property (such as a lazy-loaded collection), by dynamically "grafting" the initialized value onto the object graph. By expanding the model in this way, we have at our disposal a powerful tool for building dynamic client interfaces.

For the methods of the Model API that accept action parameters, an instance of Seam.Action should be used. The constructor for Seam.Action takes no parameters:


  var action = new Seam.Action();

The following table lists the methods used to define the action. Each of the following methods return a reference to the Seam.Action object, so methods can be chained.


The following table describes the methods provided by the Seam.Model object. To work with the Model API in JavaScript you must first create a new Model object:


  var model = new Seam.Model();

Table 54.2. Seam.Model method reference

Method

Description

addBean(alias, bean, qualifiers)

Adds a bean value to the model. When the model is fetched, the value of the specified bean will be read and placed into the model, where it may be accessed by using the getValue() method with the specified alias.

Can only be used before the model is fetched.

  • alias - the local alias for the bean value.
  • bean - the name of the bean, either specified by the @Named annotation or the fully qualified class name.
  • qualifiers (optional) - a list of bean qualifiers.

addBeanProperty(alias, bean, property, qualifiers)

Adds a bean property value to the model. When the model is fetched, the value of the specified property on the specified bean will be read and placed into the model, where it may be accessed by using the getValue() method with the specified alias.

Can only be used before the model is fetched.

Example:


  addBeanProperty("account", "AccountAction", "account", "@Qualifier1", "@Qualifier2");
  • alias - the local alias for the bean value.
  • bean - the name of the bean, either specified by the @Named annotation or the fully qualified class name.
  • property - the name of the bean property.
  • qualifiers (optional) - a list of bean qualifiers. This parameter (and any after it) are treated as bean qualifiers.

fetch(action, callback)

Fetches the model - this operation causes an asynchronous request to be sent to the server. The request contains a list of the beans and bean properties (set by calling the addBean() and addBeanProperty() methods) for which values will be returned. Once the response is received, the callback method (if specified) will be invoked, passing in a reference to the model as a parameter.

A model should only be fetched once.

  • action (optional) - a Seam.Action instance representing the bean action to invoke before the model values are read and stored in the model.
  • callback (optional) - a reference to a JavaScript function that will be invoked after the model has been fetched. A reference to the model instance is passed to the callback method as a parameter.

getValue(alias)

This method returns the value of the object with the specified alias.

  • alias - the alias of the value to return.

expand(value, property, callback)

Expands the model by initializing a property value that was previously uninitialized. This operation causes an asynchronous request to be sent to the server, where the uninitialized property value (such as a lazy-loaded collection within an entity bean association) is initialized and the resulting value is returned to the client. Once the response is received, the callback method (if specified) will be invoked, passing in a reference to the model as a parameter.

  • value - a reference to the value containing the uninitialized property to fetch. This can be any value within the model, and does not need to be a "root" value (i.e. it doesn't need to be a value specified by addBean() or addBeanProperty(), it can exist anywhere within the object graph.
  • property - the name of the uninitialized property to be initialized.
  • callback (optional) - a reference to a JavaScript function that will be invoked after the model has been expanded. A reference to the model instance is passed to the callback method as a parameter.

applyUpdates(action, callback)

Applies the changes made to the objects contained in the model. This method causes an asynchronous request to be sent to the server containing a delta consisting of a list of the changes made to the client-side objects.

  • action (optional) - a Seam.Action instance representing a bean method to be invoked after the client-side model changes have been applied to their corresponding server-side objects.
  • callback (optional) - a reference to a JavaScript function that will be invoked after the updates have been applied. A reference to the model instance is passed to the callback method as a parameter.

To fetch a model, one or more values must first be specified using addBean() or addBeanProperty() before invoking the fetch() operation. Let's work through an example - here we have an entity bean called Customer:

@Entity Customer implements Serializable {

  private Integer customerId;
  private String firstName;
  private String lastName;
  
  @Id @GeneratedValue public Integer getCustomerId() { return customerId; }  
  public void setCustomerId(Integer customerId) { this.customerId = customerId; }
  
  public String getFirstName() { return firstName; }  
  public void setFirstName(String firstName) { this.firstName = firstName; }
  
  public String getLastName() { return lastName; }
  public void setLastName(String lastName) { this.lastName = lastName; }
}

We also have a bean called CustomerAction, which is responsible for creating and editing Customer instances. Since we're only interested in editing a customer right now, the following code only shows the editCustomer() method:

@ConversationScoped @Named

public class CustomerAction {
  @Inject Conversation conversation;
  @PersistenceContext EntityManager entityManager;
  public Customer customer;
  
  public void editCustomer(Integer customerId) {
    conversation.begin();  
    customer = entityManager.find(Customer.class, customerId);
  }
  
  public void saveCustomer() {
    entityManager.merge(customer);
    conversation.end();
  }
}

In the client section of this example, we wish to make changes to an existing Customer instance, so we need to use the editCustomer() method of CustomerAction to first load the customer entity, after which we can access it via the public customer field. Our model object must therefore be configured to fetch the CustomerAction.customer property, and to invoke the editCustomer() method when the model is fetched. We start by using the addBeanProperty() method to add a bean property to the model:


  var model = new Seam.Model();
  model.addBeanProperty("customer", "CustomerAction", "customer");

The first parameter of addBeanProperty() is the alias (in this case customer), which is used to access the value via the getValue() method. The addBeanProperty() and addBean() methods can be called multiple times to bind multiple values to the model. An important thing to note is that the values may come from multiple server-side beans, they aren't all required to come from the same bean.

We also specify the action that we wish to invoke (i.e. the editCustomer() method). In this example we know the value of the customerId that we wish to edit, so we can specify this value as an action method parameter:


  var action = new Seam.Action()
    .setBeanType("CustomerAction")
    .setMethod("editCustomer")
    .addParam(123);

Once we've specified the bean properties we wish to fetch and the action to invoke, we can then fetch the model. We pass in a reference to the action object as the first parameter of the fetch() method. Also, since this is an asynchronous request we need to provide a callback method to deal with the response. The callback method is passed a reference to the model object as a parameter.


  var callback = function(model) { alert("Fetched customer: " model.getValue("customer").firstName + 
    " " + model.getValue("customer").lastName); };
  model.fetch(action, callback);

When the server receives a model fetch request, it first invokes the action (if one is specified) before reading the requested property values and returning them to the client.

Once a model has been fetched its values may be read using the getValue() method. Continuing on with the previous example, we would retrieve the Customer object via it's local alias (customer) like this:


  var customer = model.getValue("customer");

We are then free to read or modify the properties of the value (or any of the other values within its object graph).


  alert("Customer name is: " + customer.firstName + " " + customer.lastName);
  customer.setLastName("Jones"); // was Smith, but Peggy got married on the weekend

We can use the Model API's ability to expand a model to load uninitialized branches of the objects in the model's object graph. To understand how this works exactly, let's flesh out our example a little more by adding an Address entity class, and creating a one-to-many relationship between Customer and Address.

@Entity Address implements Serializable {

  private Integer addressId;
  private Customer customer;
  private String unitNumber;
  private String streetNumber;
  private String streetName;
  private String suburb;
  private String zip;
  private String state;
  private String country;
  
  @Id @GeneratedValue public Integer getAddressId() {  return addressId; }
  public void setAddressId(Integer addressId) { this.addressId = addressId; }
  
  @ManyToOne public Customer getCustomer() { return customer; }
  public void setCustomer(Customer customer) { this.customer = customer; }
  
  /* Snipped other getter/setter methods */
  
}

Here's the new field and methods that we also need to add to the Customer class:

  private Collection<Address> addresses;


  @OneToMany(fetch = FetchType.LAZY, mappedBy = "customer", cascade = CascadeType.ALL)
  public Collection<Address> getAddresses() { return addresses; }   
  public void setAddresses(Collection<Address> addresses) { this.addresses = addresses; }

As we can see, the @OneToMany annotation on the getAddresses() method specifies a fetch attribute of LAZY, meaning that by default the customer's addresses won't be loaded automatically when the customer is. When reading the uninitialized addresses property value from a newly-fetched Customer object in JavaScript, a value of undefined will be returned.


  getValue("customer").addresses == undefined; // returns true

We can expand the model by making a special request to initialize this uninitialized property value. The expand() operation takes three parameters - the value containing the property to be initialized, the name of the property and an optional callback method. The following example shows us how the customer's addresses property can be initialized:


  model.expand(model.getValue("customer"), "addresses");

The expand() operation makes an asynchronous request to the server, where the property value is initialized and the value returned to the client. When the client receives the response, it reads the initialized value and appends it to the model.


  // The addresses property now contains an array of address objects
  alert(model.getValue("customer").addresses.length + " addresses loaded");

Once you have finished making changes to the values in the model, you can apply them with the applyUpdates() method. This method scans all of the objects in the model, compares them with their original values and generates a delta which may contain one or more changesets to send to the server. A changeset is simply a list of property value changes for a single object.

Like the fetch() command you can also specify an action to invoke when applying updates, although the action is invoked after the model updates have been applied. In a typical situation the invoked action would do things like commit a database transaction, end the current conversation, etc.

Since the applyUpdates() method sends an asynchronous request like the fetch() and expand() methods, we also need to specify a callback function if we wish to do something when the operation completes.


  var action = new Seam.Action();
    .setBeanType("CustomerAction")
    .setMethod("saveCustomer");
    
  var callback = function() { alert("Customer saved."); };
     
  model.applyUpdates(action, callback);

The applyUpdates() method performs a refresh of the model, retrieving the latest state of the objects contained in the model after all updates have been applied and the action method (if specified) invoked.