Ext JS 4 models are similar to JPA entities in that they define data fields that represent columns in the underlying database tables. Each model instance represents a row in the table. The primary key field is defined using the idProperty
of the model, which must match one of the field names. The User
model can now be updated as shown:
Ext.define('TTT.model.User', {
extend: 'Ext.data.Model',
fields: [
{ name: 'username', type: 'string' },
{ name: 'firstName', type: 'string' },
{ name: 'lastName', type: 'string' },
{ name: 'fullName', type: 'string' },
{ name: 'email', type: 'string' },
{ name: 'password', type: 'string' },
{ name: 'adminRole', type: 'string' }
],
idProperty: 'username'
});
Each model can be made persistent aware by configuring an appropriate proxy. All loading and saving of data is then handled by the proxy when the load
, save
, or destroy
method on the model is called. There are several different types of proxies but the most widely used is the Ext.data.ajax.Proxy
(alternate name Ext.data.AjaxProxy
). The AjaxProxy
uses AJAX requests to read and write data from the server. Requests are sent as GET
or POST
methods depending on the operation.
A second useful proxy is Ajax.data.RestProxy
. The RestProxy
is a specialization of the AjaxProxy
that maps the four CRUD
actions to the appropriate RESTful HTTP methods (GET
, POST
, PUT
, and DELETE
). The RestProxy
would be used when connecting to RESTful web services. Our application will use AjaxProxy
.
The User
model definition including proxy follows:
Ext.define('TTT.model.User', { extend: 'Ext.data.Model', fields: [ { name: 'username', type: 'string' }, { name: 'firstName', type: 'string' }, { name: 'lastName', type: 'string' }, { name: 'fullName', type: 'string', persist:false }, { name: 'email', type: 'string' }, { name: 'password', type: 'string' }, { name: 'adminRole', type: 'string' } ], idProperty: 'username', proxy: { type: 'ajax', idParam:'username', api:{ create:'ttt/user/store.json', read:'ttt/user/find.json', update:'ttt/user/store.json', destroy:'ttt/user/remove.json' }, reader: { type: 'json', root: 'data' }, writer: { type: 'json', allowSingle:true, encode:true, root:'data', writeAllFields: true } } });
The proxy is defined as type ajax
and specifies the primary key field in the model with the idParam
property. The idParam
is used when generating the URL for the read
operation. For example, if trying to load the user record with username bjones
, the proxy would generate a URL as follows:
ttt/user/find.json?username=bjones
If the idParam
property was omitted, the URL generated would be as follows:
ttt/user/find.json?id=bjones
The api
properties define the URLs to call on CRUD action methods. Each URL maps to an appropriate handler method in UserHandler
. Note that the update
and create
URLs are the same as both actions are handled by the UserHandler.store
method.
It is important to note that the AjaxProxy
read operation uses a GET
request while all other operations use POST
requests. This is different from the RestProxy
method, which uses a different request method for each operation.
Our request handling layer has been designed to consume AJAX requests in a format submitted by Ext JS 4 clients. Each handler that processes an update action is configured with RequestMethod.POST
and expects a data
parameter that holds the JSON object applicable to the action.
We could have implemented the request handling layer as a RESTful API where each method is mapped to an appropriate request method type (GET
, POST
, PUT
, or DELETE
). Implementing a delete action would then encode the id
of the item in the URL of a DELETE
submitted request. Deleting the bjones
user, for example, could be achieved by submitting a DELETE request method URL as follows:
user/bjones
The UserHandler.remove
method could then be defined as:
@RequestMapping(value = "/user/{username}", method=RequestMethod.DELETE) @ResponseBody public String remove(final @PathVariable String username, final HttpServletRequest request) { // code continues…
The @PathVariable
extracts the username
(in our sample URL this is bjones
) from the URL, which is then used in the call to the userService.remove
method. The @RequestMapping method
of RequestMethod.DELETE
ensures the method is only executed when a DELETE request matching the URL path of /user/{username}
is submitted.
The RESTful API is a specific style of using HTTP that encodes the item you want to retrieve or manipulate in the URL itself (via its ID) and encodes what action you want to perform on it in the HTTP method used (GET
for retrieving, POST
for changing, PUT
for creating, DELETE
for deleting). The Rest
proxy in Ext JS is a specialization of the AjaxProxy
that simply maps the four CRUD actions to their RESTful HTTP equivalent method.
There is no significant difference in implementing either the AJAX or REST alternative in Ext JS 4. Configuring the proxy with type:'ajax'
or type:'rest'
is all that is required. The request handling layer, however, would need to be implemented in a very different way to process the @PathVariable
parameters. We prefer the AJAX implementation for the following reasons:
PUT
and DELETE
as HTTP methods for form
elements (see http://www.w3.org/TR/2010/WD-html5-diff-20101019/#changes-2010-06-24).PUT
and DELETE
requests are often considered a security risk (in addition to OPTIONS
, TRACE
, and CONNECT
methods) and are often disabled in enterprise web application environments. Applications that specifically require these methods (for example, web services) usually expose these URLs to a limited number of trusted users under secure conditions (usually with SSL certificates).There is no definitive or compelling reason to use AJAX over REST or vice versa. In fact the online discussions around when to use REST over AJAX are quite extensive, and often very confusing. We have chosen what we believe to be is the simplest and most flexible implementation by using AJAX without the need for REST.
The reader
with type json
instantiates a Ext.data.reader.Json
instance to decode the server's response to an operation. It reads the JSON data
node (identified by the root
property of the reader) and populates the field values in the model. Executing a read operation for the User
model using ttt/user/find.json?username=bjones
will return:
{ success: true, data: { "username": "bjones", "firstName": "Betty", "lastName": "Jones", "fullName": "Betty Jones", "email": "[email protected]", "adminRole": "Y" } }
The reader will then parse the JSON file and set the corresponding field values on the model.
The writer
with type json
instantiates an Ext.data.writer.Json
instance to encode any request sent to the server in the JSON format. The
encode:true
property combines with the root
property to define the HTTP request parameter that holds the JSON data. This combination ensures that a single request parameter with name data
will hold the JSON representation of the model. For example, saving the previous bjones
user record will result in a request being submitted with one parameter named data
holding the following string:
{ "username": "bjones", "firstName": "Betty", "lastName": "Jones", "email": "[email protected]", "password": "thepassword", "adminRole": "Y" }
It should be noted that this representation is formatted for readability; the actual data will be a string of characters on one line. This representation is then parsed into a JsonObject
in the UserHandler.store
method:
JsonObject jsonObj = parseJsonObject(jsonData);
The appropriate jsonObject
values are then extracted as required.
The writeAllFields
property will ensure that all fields in the model are sent in the request, not just the modified fields. Our handler methods require all model fields to be present. However, note that we have added the persist:false
property to the fullName
field. This field is not required as it is not a persistent field in the User
domain object.
The final writer
property that needs explanation is allowSingle:true
. This is the default value and ensures a single record is sent without a wrapping array. If your application performs bulk updates (multiple records are sent in the same single request) then you will need to set this property to false
. This would result in single records being sent within an array, as shown in the following code:
[{ "username": "bjones", "firstName": "Betty", "lastName": "Jones", "email": "[email protected]", "password": "thepassword", "adminRole": "Y" }]
The 3T application does not implement bulk updates and always expects a single JSON record to be sent in each request.
Each model has built-in support for validating field data. The core validation functions include checks for presence
, length
, inclusion
, exclusion
, format
(using regular expressions), and email
. A model instance can be validated by calling the validate
function, which returns an Ext.data.Errors
object. The errors
object can then be tested to see if there are any validation errors.
The User
model validations are as follows:
validations: [ {type: 'presence', field: 'username'}, {type: 'length', field: 'username', min: 4}, {type: 'presence', field: 'firstName'}, {type: 'length', field: 'firstName', min: 2}, {type: 'presence', field: 'lastName'}, {type: 'length', field: 'lastName', min: 2}, {type: 'presence', field: 'email'}, {type: 'email', field: 'email'}, {type: 'presence', field: 'password'}, {type: 'length', field: 'password', min: 6}, {type: 'inclusion', field: 'adminRole', list:['Y','N']} ]
The presence
validation ensures that a value is present for the field. The length
validation checks for field size. Our validations require a minimum password
size of six characters and a minimum username
of four characters. First and last names have a minimum size of two characters. The inclusion
validation tests to ensure the field value is one of the entries in the defined list. As a result, our adminRole
value must be either a Y
or N
. The email
validation ensures the e-mail field has a valid e-mail format.
The final code listing for our User
model can now be defined as:
Ext.define('TTT.model.User', { extend: 'Ext.data.Model', fields: [ { name: 'username', type: 'string' }, { name: 'firstName', type: 'string' }, { name: 'lastName', type: 'string' }, { name: 'fullName', type: 'string', persist:false }, { name: 'email', type: 'string' }, { name: 'password', type: 'string' }, { name: 'adminRole', type: 'string' } ], idProperty: 'username', proxy: { type: 'ajax', idParam:'username', api:{ create:'ttt/user/store.json', read:'ttt/user/find.json', update:'ttt/user/store.json', destroy:'ttt/user/remove.json' }, reader: { type: 'json', root: 'data' }, writer: { type: 'json', allowSingle:true, encode:true, root:'data', writeAllFields: true } }, validations: [ {type: 'presence', field: 'username'}, {type: 'length', field: 'username', min: 4}, {type: 'presence', field: 'firstName'}, {type: 'length', field: 'firstName', min: 2}, {type: 'presence', field: 'lastName'}, {type: 'length', field: 'lastName', min: 2}, {type: 'presence', field: 'email'}, {type: 'email', field: 'email'}, {type: 'presence', field: 'password'}, {type: 'length', field: 'password', min: 6}, {type: 'inclusion', field: 'adminRole', list:['Y','N']} ] });
18.190.217.253