PDA

View Full Version : Ext.data.Model, REST and Model Associations



marcolara
12 Jan 2012, 12:15 PM
Good afternoon!
I have an interesting implementation I would like to do with Models and Associations. If you are familiar with Hibernate in the Java world, you will be familiar with the way POJOs are associated with each other based on constrains though one-to-many, many-to-one etc... well, ExtJS Models have a similar concept with it's Associations and I started exploring this front. I'm able to create associations, but I can't quite make these models send RESTful requests that can be match by my Web Application.

Let me start by explaining y data and rest patterns:

I have the following REST URI patters:

POST => /category/
PUT => /category/123/
DELETE => /category/123/
POST => /category/123/product/
PUT => /category/123/product/321/
DELETE => /category/123/product/321/
POST => /category/123/product/321/feature/
PUT => /category/123/product/321/feature/345
DELETE => /category/123/product/321/feature/345

And the following Ext.data.Model models:



// ==================

Ext.data.writer.Json.override({
/*
* This function overrides the default implementation of json writer. Any hasMany relationships will be submitted
* as nested objects. When preparing the data, only children which have been newly created, modified or marked for
* deletion will be added. To do this, a depth first bottom -> up recursive technique was used.
*/
getRecordData: function(record) {
//Setup variables
var me = this, i, association, childStore, data = {};
if(record.proxy.writer.writeAllFields){
data = record.data;
}
else {
var changes, name, field, fields = record.fields, nameProperty = this.nameProperty, key;
changes = record.getChanges();
for (key in changes) {
if (changes.hasOwnProperty(key)) {
field = fields.get(key);
name = field[nameProperty] || field.name;
data[name] = changes[key];
}
}
if (!record.phantom) {
// always include the id for non phantoms
data[record.idProperty] = record.getId();
}
}

//Iterate over all the hasMany associations
for (i = 0; i < record.associations.length; i++) {
association = record.associations.get(i);
if (association.type == 'hasMany') { // <-- this avoids null reference issue
data[association.name] = [];
childStore = record[association.storeName];

// if no store is return
if (childStore != undefined){
//Iterate over all the children in the current association
childStore.each(function(childRecord) {

//Recursively get the record data for children (depth first)
var childData = this.getRecordData.call(this, childRecord);

/*
* If the child was marked dirty or phantom it must be added. If there was data returned that was neither
* dirty or phantom, this means that the depth first recursion has detected that it has a child which is
* either dirty or phantom. For this child to be put into the prepared data, it's parents must be in place whether
* they were modified or not.
*/
if (childRecord.dirty | childRecord.phantom | (childData != null)){
data[association.name].push(childData);
record.setDirty();
}
}, me);


/*
* Iterate over all the removed records and add them to the preparedData. Set a flag on them to show that
* they are to be deleted
*/
Ext.each(childStore.removed, function(removedChildRecord) {
//Set a flag here to identify removed records
removedChildRecord.set('forDeletion', true);
var removedChildData = this.getRecordData.call(this, removedChildRecord);
data[association.name].push(removedChildData);
record.setDirty();
}, me);
}
}
}


//Only return data if it was dirty, new or marked for deletion.
if (record.dirty | record.phantom | record.get('forDeletion')){
return data;
}
}
});

Ext.data.Store.override({
remove: function(records, /* private */ isMove) {
if (!Ext.isArray(records)) {
records = [records];
}
/*
* Pass the isMove parameter if we know we're going to be re-inserting this record
*/
isMove = isMove === true;
var me = this,
sync = false,
i = 0,
length = records.length,
isPhantom,
index,
record;
for (; i < length; i++) {
record = records[i];
index = me.data.indexOf(record);
if (me.snapshot) {
me.snapshot.remove(record);
}
if (index > -1) {
isPhantom = record.phantom === true;
if (!isMove && !isPhantom) {
// don't push phantom records onto removed
record.set('forDeletion', true);
me.removed.push(record);
}
record.unjoin(me);
me.data.remove(record);
sync = sync || !isPhantom;
me.fireEvent('remove', me, record, index);
}
}
me.fireEvent('datachanged', me);
if (!isMove && me.autoSync && sync) {
me.sync();
}
}
});
Ext.data.TreeStore.override({
onNodeRemove: function(parent, node) {
var removed = this.removed;
if (!node.isReplace && Ext.Array.indexOf(removed, node) == -1) {
node.set('forDeletion', true);
removed.push(node);
}
}
});

//Categories
Ext.define('CategoryProxy', {
extend : 'Ext.data.proxy.Rest',
type : 'rest',
url : '/category/',
appendId : true,
reader : {
type : 'json',
root : 'data'
},
writer : {
type : 'json'
}
});

Ext.define (http://docs.sencha.com/ext-js/4-0/#%21/api/Ext-method-define)('Category', { extend: 'Ext.data.Model (http://docs.sencha.com/ext-js/4-0/#%21/api/Ext.data.Model)', fields: ['id','name'],
idProperty : 'id',
hasMany: [{model: 'Product', name: 'products'}],
proxy: Ext.create('CategoryProxy') });


// ==================
//Products
Ext.define('ProductProxy', {
extend : 'Ext.data.proxy.Rest',
type : 'rest',
url : '/category/{categoryId}/product/',
appendId : true,
reader : {
type : 'json',
root : 'data'
},
writer : {
type : 'json'
}
});
Ext.override(ProductProxy, {
buildUrl: function(request){
operation = request.operation,
records = operation.records || [],
record = records[0],
format = this.format,
url = this.getUrl(request),
id = record ? record.getId() : operation.id;

///TODO: <== CODE TO REPLACE {categoryId} FROM THE PROXY URL
/// WITH THE CATEGORY ID "id" FROM THE
/// ASSOCIATED CATEGORY MODEL

return this.callParent(arguments);
}
});
Ext.define (http://docs.sencha.com/ext-js/4-0/#%21/api/Ext-method-define)('Product', { extend: 'Ext.data.Model (http://docs.sencha.com/ext-js/4-0/#%21/api/Ext.data.Model)', fields: ['id','name','description'],
idProperty : 'id',
belongsTo: 'Category',
hasMany: [{model: 'Feature', name: 'features'}],
proxy: Ext.create('ProductProxy') });


// ==================
// Features
Ext.define('FeatureProxy', {
extend : 'Ext.data.proxy.Rest',
type : 'rest',
url : '/category/{categoryId}/product/{productId}/feature',
appendId : true,
reader : {
type : 'json',
root : 'data'
},
writer : {
type : 'json'
}
});
Ext.override(FeatureProxy, {
buildUrl: function(request){
operation = request.operation,
records = operation.records || [],
record = records[0],
format = this.format,
url = this.getUrl(request),
id = record ? record.getId() : operation.id;

///TODO: <== CODE TO REPLACE {categoryId} AND {productId} FROM THE PROXY URL
/// WITH THE CATEGORY ID "id" AND THE PRODUCT ID "id" FROM THE
/// ASSOCIATED CATEGORY AND PRODUCT MODELS

return this.callParent(arguments);
}
});

Ext.define (http://docs.sencha.com/ext-js/4-0/#%21/api/Ext-method-define)('Feature', { extend: 'Ext.data.Model (http://docs.sencha.com/ext-js/4-0/#%21/api/Ext.data.Model)', fields: ['id','name'],
idProperty : 'id',
belongsTo: 'Product',
proxy: Ext.create('FeatureProxy') });




Now as you can see, I have created custom "buildUrl" methods to replace (somehow) the variables inside the curly brackets and this is one of my many questions. Let's see and create a new object and send a request:



var category = Ext.create('Category', {
name: 'Cars'
});
var products = category.products();
var pOne = Ext.create('Product', {
name: 'my car',
description: 'One badass car'
});
var pTwo = Ext.create('Product', {
name: 'your car',
description: 'One cool car'
});
var feat = Ext.create('Feature', {
name: '4 wheels'
});

var featsOne = pOne.features();
featsOne.add(feat);
featsOne.sync();
var featsTwo = pTwo.features();
featsTwo.add(feat);
featsTwo.sync();

products.add(pOne);
products.add(pTwo);
products.sync();

category.save();



What this creates is a 4 request that looks like this to the following URL and in this order:


POST => http://localhost:8080/category/{categoryId}/product/{productId}/feature?_dc=1326397365191
{"name":"4 wheels","id":"","product_id":""}

POST => http://localhost:8080/category/{categoryId}/product/{productId}/feature?_dc=1326397365199
{"name":"4 wheels","id":"","product_id":""}

POST => http://localhost:8080/category/{categoryId}/product/?_dc=1326397365203
{"name":"my car","description":"One badass car","id":"","category_id":"","features":[{"name":"4 wheels","id":"","product_id":""}]}

POST => http://localhost:8080/category/?_dc=1326397365207
{"name":"Cars","id":"","products":[{"name":"my car","description":"One badass car","id":"","category_id":"","features":[{"name":"4 wheels","id":"","product_id":""}]},{"name":"your car","description":"One cool car","id":"","category_id":"","features":[{"name":"4 wheels","id":"","product_id":""}]}]}



Well, this is OK except that I only need the last request since it has all of the associations "products" and "features" in the requests. The Java App should get this request, see that it's a new product request and create all of the data in the DB and hand back a JSON object with the "id" for all objects populated like so and the product reader should read the response JSON and populate accordingly:


{
"request_id": 1231221,
"data": {
"name": "Cars",
"id": "344",
"products": [
{
"name": "my car",
"description": "One badass car",
"id": "4324",
"category_id": "",
"features": [
{
"name": "4 wheels",
"id": "34",
"product_id": ""
}
]
},
{
"name": "your car",
"description": "One cool car",
"id": "654",
"category_id": "",
"features": [
{
"name": "4 wheels",
"id": "5656",
"product_id": ""
}
]
}
]
}
}

so the next time I call product.save() it should send PUT requests as described above.

Conceptually, Extjs should be able to support what I want with some modifications, however... I wanted to know if I can do this without modifying the core by just changing some behavior through it's configuration of objects.

Thanks!

pyoungerv
7 Apr 2012, 9:52 AM
i want to do the same thing. have you had any success?