SeamFramework.orgCommunity Documentation
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.
Table 54.1. Seam.Action method reference
Method |
Description |
---|---|
|
Sets the class name of the bean to be invoked.
|
|
Sets the qualifiers for the bean to be invoked.
|
|
Sets the name of the bean method.
|
|
Adds a parameter value for the action method. This method should be called once for each parameter value to be added, in the correct parameter order.
|
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 |
---|---|
|
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
Can only be used before the model is fetched.
|
|
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 Can only be used before the model is fetched. Example: addBeanProperty("account", "AccountAction", "account", "@Qualifier1", "@Qualifier2");
|
|
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
A model should only be fetched once.
|
|
This method returns the value of the object with the specified alias.
|
|
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.
|
|
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.
|
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.
Alternatively, if you don't wish to fetch a bean property but rather a bean itself
(such as a value created by a producer method) then the addBean()
method is used instead.
Let's say we have a producer method that returns a qualified UserSettings
value:
@Produces @ConversationScoped @Settings UserSettings getUserSettings() {
/* snip code */
}
We would add this value to our model with the following code:
model.addBean("settings", "UserSettings", "@Settings");
The first parameter is the local alias for the value, the second parameter is the fully qualified class of the bean, and the third (and subsequent) parameter/s are optional bean qualifiers.
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.