Merge pull request #141 from strongloop/feature/fix-perf

Feature/fix perf
This commit is contained in:
Raymond Feng 2014-06-18 22:40:00 -07:00
commit 7c71e1e53b
5 changed files with 213 additions and 218 deletions

View File

@ -162,7 +162,6 @@ DataAccessObject.create = function (data, callback) {
this._adapter().create(modelName, this.constructor._forDB(obj.toObject(true)), function (err, id, rev) {
if (id) {
obj.__data[_idName] = id;
obj.__dataWas[_idName] = id;
defineReadonlyProp(obj, _idName, id);
}
if (rev) {
@ -353,8 +352,7 @@ DataAccessObject.findById = function find(id, cb) {
if (!getIdValue(this, data)) {
setIdValue(this, data, id);
}
obj = new this();
obj._initProperties(data);
obj = new this(data, {applySetters: false});
}
cb(err, obj);
}.bind(this));
@ -689,9 +687,7 @@ DataAccessObject.find = function find(query, cb) {
this.getDataSource().connector.all(this.modelName, query, function (err, data) {
if (data && data.forEach) {
data.forEach(function (d, i) {
var obj = new self();
obj._initProperties(d, {fields: query.fields});
var obj = new self(d, {fields: query.fields, applySetters: false});
if (query && query.include) {
if (query.collect) {
@ -1059,12 +1055,6 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
}
inst._adapter().updateAttributes(model, getIdValue(inst.constructor, inst), inst.constructor._forDB(typedData), function (err) {
if (!err) {
// update $was attrs
for (var key in data) {
inst.__dataWas[key] = inst.__data[key];
}
}
done.call(inst, function () {
saveDone.call(inst, function () {
if(cb) cb(err, inst);

View File

@ -264,7 +264,11 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
// A function to loop through the properties
ModelClass.forEachProperty = function (cb) {
Object.keys(ModelClass.definition.properties).forEach(cb);
var props = ModelClass.definition.properties;
var keys = Object.keys(props);
for (var i = 0, n = keys.length; i < n; i++) {
cb(keys[i], props[keys[i]]);
}
};
// A function to attach the model class to a data source
@ -309,27 +313,22 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
}
// Merging the properties
Object.keys(properties).forEach(function (key) {
var keys = Object.keys(properties);
for (var i = 0, n = keys.length; i < n; i++) {
var key = keys[i];
if (idFound && properties[key].id) {
// don't inherit id properties
return;
continue;
}
if (subclassProperties[key] === undefined) {
subclassProperties[key] = properties[key];
}
});
}
// Merge the settings
subclassSettings = mergeSettings(settings, subclassSettings);
/*
Object.keys(settings).forEach(function (key) {
if(subclassSettings[key] === undefined) {
subclassSettings[key] = settings[key];
}
});
*/
// Define the subclass
var subClass = modelBuilder.define(className, subclassProperties, subclassSettings, ModelClass);
@ -370,20 +369,15 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
var DataType = ModelClass.definition.properties[propertyName].type;
if (Array.isArray(DataType) || DataType === Array) {
DataType = List;
} else if (DataType.name === 'Date') {
var OrigDate = Date;
DataType = function Date(arg) {
return new OrigDate(arg);
};
} else if (DataType === Date) {
DataType = DateType;
} else if (typeof DataType === 'string') {
DataType = modelBuilder.resolveType(DataType);
}
if (ModelClass.setter[propertyName]) {
ModelClass.setter[propertyName].call(this, value); // Try setter first
} else {
if (!this.__data) {
this.__data = {};
}
this.__data = this.__data || {};
if (value === null || value === undefined) {
this.__data[propertyName] = value;
} else {
@ -401,15 +395,6 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
enumerable: true
});
// <propertyName>$was --> __dataWas.<propertyName>
Object.defineProperty(ModelClass.prototype, propertyName + '$was', {
get: function () {
return this.__dataWas && this.__dataWas[propertyName];
},
configurable: true,
enumerable: false
});
// FIXME: [rfeng] Do we need to keep the raw data?
// Use $ as the prefix to avoid conflicts with properties such as _id
Object.defineProperty(ModelClass.prototype, '$' + propertyName, {
@ -427,7 +412,13 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
});
};
ModelClass.forEachProperty(ModelClass.registerProperty);
var props = ModelClass.definition.properties;
var keys = Object.keys(props);
var size = keys.length;
for (i = 0; i < size; i++) {
var propertyName = keys[i];
ModelClass.registerProperty(propertyName);
}
ModelClass.emit('defined', ModelClass);
@ -435,6 +426,11 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
};
// DataType for Date
function DateType(arg) {
return new Date(arg);
}
/**
* Define single property named `propertyName` on `model`
*
@ -479,10 +475,11 @@ ModelBuilder.prototype.defineProperty = function (model, propertyName, propertyD
*/
ModelBuilder.prototype.extendModel = function (model, props) {
var t = this;
Object.keys(props).forEach(function (propName) {
var definition = props[propName];
t.defineProperty(model, propName, definition);
});
var keys = Object.keys(props);
for (var i = 0; i < keys.length; i++) {
var definition = props[keys[i]];
t.defineProperty(model, keys[i], definition);
}
};
ModelBuilder.prototype.copyModel = function copyModel(Master) {

View File

@ -8,13 +8,20 @@ module.exports = ModelBaseClass;
*/
var util = require('util');
var traverse = require('traverse');
var jutil = require('./jutil');
var List = require('./list');
var Hookable = require('./hooks');
var validations = require('./validations.js');
var BASE_TYPES = ['String', 'Boolean', 'Number', 'Date', 'Text', 'ObjectID'];
// Set up an object for quick lookup
var BASE_TYPES = {
'String': true,
'Boolean': true,
'Number': true,
'Date': true,
'Text': true,
'ObjectID': true
};
/**
* Model class: base class for all persistent objects.
@ -33,18 +40,6 @@ function ModelBaseClass(data, options) {
this._initProperties(data, options);
}
// FIXME: [rfeng] We need to make sure the input data should not be mutated. Disabled cloning for now to get tests passing
function clone(data) {
/*
if(!(data instanceof ModelBaseClass)) {
if(data && (Array.isArray(data) || 'object' === typeof data)) {
return traverse(data).clone();
}
}
*/
return data;
}
/**
* Initialize the model instance with a list of properties
* @param {Object} data The data object
@ -58,10 +53,10 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
var ctor = this.constructor;
if(data instanceof ctor) {
// Convert the data to be plain object to avoid polutions
// Convert the data to be plain object to avoid pollutions
data = data.toObject(false);
}
var properties = ctor.definition.build();
var properties = ctor.definition.properties;
data = data || {};
options = options || {};
@ -71,133 +66,124 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
if(strict === undefined) {
strict = ctor.definition.settings.strict;
}
Object.defineProperty(this, '__cachedRelations', {
writable: true,
enumerable: false,
configurable: true,
value: {}
});
Object.defineProperty(this, '__data', {
writable: true,
enumerable: false,
configurable: true,
value: {}
});
if (ctor.hideInternalProperties) {
// Object.defineProperty() is expensive. We only try to make the internal
// properties hidden (non-enumerable) if the model class has the
// `hideInternalProperties` set to true
Object.defineProperties(this, {
__cachedRelations: {
writable: true,
enumerable: false,
configurable: true,
value: {}
},
Object.defineProperty(this, '__dataWas', {
writable: true,
enumerable: false,
configurable: true,
value: {}
});
__data: {
writable: true,
enumerable: false,
configurable: true,
value: {}
},
/**
* Instance level data source
*/
Object.defineProperty(this, '__dataSource', {
writable: true,
enumerable: false,
configurable: true,
value: options.dataSource
});
// Instance level data source
__dataSource: {
writable: true,
enumerable: false,
configurable: true,
value: options.dataSource
},
/**
* Instance level strict mode
*/
Object.defineProperty(this, '__strict', {
writable: true,
enumerable: false,
configurable: true,
value: strict
});
// Instance level strict mode
__strict: {
writable: true,
enumerable: false,
configurable: true,
value: strict
}
});
} else {
this.__cachedRelations = {};
this.__data = {};
this.__dataSource = options.dataSource;
this.__strict = strict;
}
if (data.__cachedRelations) {
this.__cachedRelations = data.__cachedRelations;
}
for (var i in data) {
if (i in properties && typeof data[i] !== 'function') {
this.__data[i] = this.__dataWas[i] = clone(data[i]);
} else if (i in ctor.relations) {
if (ctor.relations[i].type === 'belongsTo' && data[i] !== null && data[i] !== undefined) {
var keys = Object.keys(data);
var size = keys.length;
var p, propVal;
for (var k = 0; k < size; k++) {
p = keys[k];
propVal = data[p];
if (typeof propVal === 'function') {
continue;
}
if (properties[p]) {
// Managed property
if (applySetters) {
self[p] = propVal;
} else {
self.__data[p] = propVal;
}
} else if (ctor.relations[p]) {
// Relation
if (ctor.relations[p].type === 'belongsTo' && propVal != null) {
// If the related model is populated
this.__data[ctor.relations[i].keyFrom] = this.__dataWas[i] = data[i][ctor.relations[i].keyTo];
self.__data[ctor.relations[p].keyFrom] = propVal[ctor.relations[p].keyTo];
}
this.__cachedRelations[i] = data[i];
self.__cachedRelations[p] = propVal;
} else {
// Un-managed property
if (strict === false) {
this.__data[i] = this.__dataWas[i] = clone(data[i]);
self[p] = self.__data[p] = propVal;
} else if (strict === 'throw') {
throw new Error('Unknown property: ' + i);
throw new Error('Unknown property: ' + p);
}
}
}
var propertyName;
if (applySetters === true) {
for (propertyName in data) {
if (typeof data[propertyName] !== 'function' && ((propertyName in properties) || (propertyName in ctor.relations))) {
self[propertyName] = self.__data[propertyName] || data[propertyName];
keys = Object.keys(properties);
size = keys.length;
for (k = 0; k < size; k++) {
p = keys[k];
propVal = self.__data[p];
// Set default values
if (propVal === undefined) {
var def = properties[p]['default'];
if (def !== undefined) {
if (typeof def === 'function') {
self.__data[p] = def();
} else {
self.__data[p] = def;
}
}
}
}
// Set the unknown properties as properties to the object
if (strict === false) {
for (propertyName in data) {
if (typeof data[propertyName] !== 'function' && !(propertyName in properties)) {
self[propertyName] = self.__data[propertyName] || data[propertyName];
}
}
}
ctor.forEachProperty(function (propertyName) {
if (undefined === self.__data[propertyName]) {
self.__data[propertyName] = self.__dataWas[propertyName] = getDefault(propertyName);
} else {
self.__dataWas[propertyName] = self.__data[propertyName];
}
});
ctor.forEachProperty(function (propertyName) {
var type = properties[propertyName].type;
if (BASE_TYPES.indexOf(type.name) === -1) {
if (typeof self.__data[propertyName] !== 'object' && self.__data[propertyName]) {
// Handle complex types (JSON/Object)
var type = properties[p].type;
if (! BASE_TYPES[type.name]) {
if (typeof self.__data[p] !== 'object' && self.__data[p]) {
try {
self.__data[propertyName] = JSON.parse(self.__data[propertyName] + '');
self.__data[p] = JSON.parse(self.__data[p] + '');
} catch (e) {
self.__data[propertyName] = String(self.__data[propertyName]);
self.__data[p] = String(self.__data[p]);
}
}
if (type.name === 'Array' || Array.isArray(type)) {
if (!(self.__data[propertyName] instanceof List)
&& self.__data[propertyName] !== undefined
&& self.__data[propertyName] !== null ) {
self.__data[propertyName] = List(self.__data[propertyName], type, self);
if (!(self.__data[p] instanceof List)
&& self.__data[p] !== undefined
&& self.__data[p] !== null ) {
self.__data[p] = List(self.__data[p], type, self);
}
}
}
});
function getDefault(propertyName) {
var def = properties[propertyName]['default'];
if (def !== undefined) {
if (typeof def === 'function') {
return def();
} else {
return def;
}
} else {
return undefined;
}
}
this.trigger('initialize');
};
@ -242,7 +228,7 @@ ModelBaseClass.toString = function () {
* @param {Boolean} onlySchema Restrict properties to dataSource only. Default is false. If true, the function returns only properties defined in the schema; Otherwise it returns all enumerable properties.
*/
ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) {
if(onlySchema === undefined) {
if (onlySchema === undefined) {
onlySchema = true;
}
var data = {};
@ -250,47 +236,63 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) {
var Model = this.constructor;
// if it is already an Object
if(Model === Object) return self;
if (Model === Object) {
return self;
}
var strict = this.__strict;
var schemaLess = (strict === false) || !onlySchema;
this.constructor.forEachProperty(function (propertyName) {
if (removeHidden && Model.isHiddenProperty(propertyName)) {
return;
}
if (typeof self[propertyName] === 'function') {
return;
}
if (self[propertyName] instanceof List) {
data[propertyName] = self[propertyName].toObject(!schemaLess, removeHidden);
} else if (self.__data.hasOwnProperty(propertyName)) {
if (self[propertyName] !== undefined && self[propertyName] !== null && self[propertyName].toObject) {
data[propertyName] = self[propertyName].toObject(!schemaLess, removeHidden);
} else {
data[propertyName] = self[propertyName];
}
} else {
data[propertyName] = null;
}
});
var props = Model.definition.properties;
var keys = Object.keys(props);
var propertyName, val;
for (var i = 0; i < keys.length; i++) {
propertyName = keys[i];
val = self[propertyName];
// Exclude functions
if (typeof val === 'function') {
continue;
}
// Exclude hidden properties
if (removeHidden && Model.isHiddenProperty(propertyName)) {
continue;
}
if (val instanceof List) {
data[propertyName] = val.toObject(!schemaLess, removeHidden);
} else {
if (val !== undefined && val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess, removeHidden);
} else {
data[propertyName] = val;
}
}
}
var val = null;
if (schemaLess) {
// Find its own properties which can be set via myModel.myProperty = 'myValue'.
// If the property is not declared in the model definition, no setter will be
// triggered to add it to __data
for (var propertyName in self) {
if(removeHidden && Model.isHiddenProperty(propertyName)) {
keys = Object.keys(self);
var size = keys.length;
for (i = 0; i < size; i++) {
propertyName = keys[i];
if (props[propertyName]) {
continue;
}
if(self.hasOwnProperty(propertyName) && (!data.hasOwnProperty(propertyName))) {
val = self[propertyName];
if (propertyName.indexOf('__') === 0) {
continue;
}
if (removeHidden && Model.isHiddenProperty(propertyName)) {
continue;
}
val = self[propertyName];
if (val !== undefined && data[propertyName] === undefined) {
if (typeof val === 'function') {
continue;
}
if (val !== undefined && val !== null && val.toObject) {
if (val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess, removeHidden);
} else {
data[propertyName] = val;
@ -298,15 +300,25 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) {
}
}
// Now continue to check __data
for (propertyName in self.__data) {
if (!data.hasOwnProperty(propertyName)) {
if(removeHidden && Model.isHiddenProperty(propertyName)) {
keys = Object.keys(self.__data);
size = keys.length;
for (i = 0; i < size; i++) {
propertyName = keys[i];
if (propertyName.indexOf('__') === 0) {
continue;
}
if (data[propertyName] === undefined) {
if (removeHidden && Model.isHiddenProperty(propertyName)) {
continue;
}
val = self.hasOwnProperty(propertyName) ? self[propertyName] : self.__data[propertyName];
var ownVal = self[propertyName];
// The ownVal can be a relation function
val = (ownVal !== undefined && (typeof ownVal !== 'function'))
? ownVal : self.__data[propertyName];
if (typeof val === 'function') {
continue;
}
if (val !== undefined && val !== null && val.toObject) {
data[propertyName] = val.toObject(!schemaLess, removeHidden);
} else {
@ -319,12 +331,20 @@ ModelBaseClass.prototype.toObject = function (onlySchema, removeHidden) {
return data;
};
ModelBaseClass.isHiddenProperty = function(propertyName) {
ModelBaseClass.isHiddenProperty = function (propertyName) {
var Model = this;
var settings = Model.definition && Model.definition.settings;
var hiddenProperties = settings && settings.hidden;
if(hiddenProperties) {
return ~hiddenProperties.indexOf(propertyName);
var hiddenProperties = settings && (settings.hiddenProperties || settings.hidden);
if (Array.isArray(hiddenProperties)) {
// Cache the hidden properties as an object for quick lookup
settings.hiddenProperties = {};
for (var i = 0; i < hiddenProperties.length; i++) {
settings.hiddenProperties[hiddenProperties[i]] = true;
}
hiddenProperties = settings.hiddenProperties;
}
if (hiddenProperties) {
return hiddenProperties[propertyName];
} else {
return false;
}
@ -340,16 +360,6 @@ ModelBaseClass.prototype.fromObject = function (obj) {
}
};
/**
* Checks is property changed based on current property and initial value
*
* @param {String} propertyName Property name
* @return Boolean
*/
ModelBaseClass.prototype.propertyChanged = function propertyChanged(propertyName) {
return this.__data[propertyName] !== this.__dataWas[propertyName];
};
/**
* Reset dirty attributes.
* This method does not perform any database operations; it just resets the object to its
@ -361,9 +371,6 @@ ModelBaseClass.prototype.reset = function () {
if (k !== 'id' && !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) {
delete obj[k];
}
if (obj.propertyChanged(k)) {
obj[k] = obj[k + '$was'];
}
}
};

View File

@ -1299,15 +1299,24 @@ describe('Load models from json', function () {
customer.should.not.have.property('bio');
// The properties are defined at prototype level
assert.equal(Object.keys(customer).length, 0);
assert.equal(Object.keys(customer).filter(function (k) {
// Remove internal properties
return k.indexOf('__') === -1;
}).length, 0);
var count = 0;
for (var p in customer) {
if (p.indexOf('__') === 0) {
continue;
}
if (typeof customer[p] !== 'function') {
count++;
}
}
assert.equal(count, 7); // Please note there is an injected id from User prototype
assert.equal(Object.keys(customer.toObject()).length, 6);
assert.equal(Object.keys(customer.toObject()).filter(function (k) {
// Remove internal properties
return k.indexOf('__') === -1;
}).length, 6);
done(null, customer);
});

View File

@ -133,14 +133,11 @@ describe('manipulation', function () {
Person.findOne(function (err, p) {
should.not.exist(err);
p.name = 'Hans';
p.propertyChanged('name').should.be.true;
p.save(function (err) {
should.not.exist(err);
p.propertyChanged('name').should.be.false;
Person.findOne(function (err, p) {
should.not.exist(err);
p.name.should.equal('Hans');
p.propertyChanged('name').should.be.false;
done();
});
});
@ -157,10 +154,8 @@ describe('manipulation', function () {
p.name = 'Nana';
p.save(function (err) {
should.exist(err);
p.propertyChanged('name').should.be.true;
p.save({validate: false}, function (err) {
should.not.exist(err);
p.propertyChanged('name').should.be.false;
done();
});
});
@ -244,10 +239,7 @@ describe('manipulation', function () {
person = new Person({name: hw});
person.name.should.equal(hw);
person.propertyChanged('name').should.be.false;
person.name = 'Goodbye, Lenin';
person.name$was.should.equal(hw);
person.propertyChanged('name').should.be.true;
(person.createdAt >= now).should.be.true;
person.isNewRecord().should.be.true;
});