Just Another attempt at DOA Getters and Setters#538
Conversation
… DAOs such that custom object property getter/setter functions can be applied to a DAO instance (not yet implemented, but I suspect in to be defined along-side "instanceMethods"), all attribute values ar enow stored in a "dataValues" property and transparently made available for setting and getting via object property get/set functions
…ct and causes an infinite loop when setting a data value for a field that is as yet unset (via a property setter that has the same name as the original property)
|
i wonder if this PR will change the location of the values in a model's instance. will it move from instance.foo to instance.dataValues.foo ? |
|
@sdepold that is exactly what happens with explicitly defined 'data property' values of the model, for each and every property created on the 'internal' 'association' properties (as generated/set by the relevant mixins) are still just stuck on the actual data object instance (i.e. as such the fact that actual data-values exist in I understand this is quite a tricky change to incorporate and that it may require work to get it into a shape you are willing to merge ... please instruct me if you are seriously interested in this patch (I would be willing to write some docs after a merge). as a sidenote I am running this PR in my code (MySQL project) and it is working nicely for me. |
|
So you store the values in dataValues instead of the DAO Object and then just map from the Object to dataValues with getters/setters? |
|
setDataValues & getDataValues could probably also be names set/get, and then just have the getters/setters on the object map to those, and then handle custom getter/setter logic there |
|
@mickhansen I did not name the property I would advise against naming the 'real' data-value getting and setting functions simply currently the default getters/setter are defined as such: if (has !== true) {
this.__defineGetter__(attribute, has.get || function() { return this.dataValues[attribute]; });
this.__defineSetter__(attribute, has.set || function(v) { this.dataValues[attribute] = v; });
} I suppose these could be changed like so (untested) - but I chose not to put another function call on the stack: if (has !== true) {
this.__defineGetter__(attribute, has.get || this.getDataValue);
this.__defineSetter__(attribute, has.set || this.setDataValue);
} I don't think it really matters either way, I implemented look forward to hearing if there are any changes that I need to make in order to get this PR through! |
|
Well, personally i'd like to see my code move from using model.attribute to model.get('attribute') in the feature. Currently .values is a custom getter that returns values + eagerly loaded associations, but that should arguebly be a job for toJSON - seems weird to me to have both values and dataValues. My argument for using set/get methods on objct would be that it would set/get both on dataValues, the object, and handle custom getters/setters - so we could keep all that logic in one place. |
|
@mickhansen - trying to stick every use-case into the form assume instance.set = function(a, v) { this[a] = v; }
// endless loop here:
instance.__defineSetter__('name', function(v) { this.set('name', v.toString().toUpperCase()); });
// name attribute set okay here:
instance.__defineSetter__('name', function(v) { this.setDataValues('name', v.toString().toUpperCase()); });put another way, ... I see adding public I also did think that I would be easy to implement 'public' |
|
Ah, yeah i think i get your point now. Unless he of course implements an opposite getter. |
|
I see that your code takes care of implementing an opposite getter/setter if not defined actually. |
|
hey @mickhansen. yes each (non-associative) attribute that exists will always have a a getter and setter defined (if the model doesn't define a specific getter or setter for an attribute a 'std' one will be setup). ...to try and clarify the use of given a model that defines a "name" attribute and a custom setter function for "name", the definition of the custom setter must be like so: function(v) { this.setDataValue('name', v.toString().toUpperCase()); }note that the implementor of the custom "name" setter only uses the method Application code (i.e. outside the model definition) would set the name attribute as follows (i.e. nothing changes for code that 'consumes' model instances): instance.name = "bart";
function(v) { this.dataValues.name = v.toString().toUpperCase(); }that would work but now someone's model definition tightly coupled to the Now imagine a similar model with a "name" attribute, one custom "name_low" getter and one custom "name_up" setter. the property setter/getter implementations can be done as follows (note that these could be considered as pseudo-attributes): // a bit more of the surrounding boiler plate code to give context:
Sequelize.define({
id : { type: Sequelize.INTEGER, autoIncrement: true, primaryKey: true },
name : { type: Sequelize.STRING, allowNull: false }
}, {
getterMethods : {
name_low: function() { this.name.toString().toLowerCase(); }
},
setterMethods : {
name_up : function(v) {this.name = v.toString().toUpperCase(); }
}
});usage of this model would like so: instance.name = 'foo'; // name attribute now equal 'foo'
instance.name_up = 'foo'; // name attribute now equal 'FOO'
console.log(instance.name_low) // outputs 'foo' even though name attribute is equal to 'foo' ... possibly my explainations are about as clear as mud, hopefully not :) |
|
I see, now i understand your design - I think my confusion stems from me having a different design in my head initially (i also thought of implementing this). I was considering doing something like: |
|
I believe there is a significant performance penalty for using property getter/setters when compared to the speed of using straight forward properties on a model instance - this conclusion is based on a bit of searching for relevant benchmarks. if you wish to keep the public interface for data attributes in it's current form (i.e. being able to do |
|
@iamjochem any chance you can update your branch to the latest sequelize changes? Also cc @janmeier @sdepold @mickhansen :) |
|
@durango how should I do that, I imagine rebasing the branch on which this PR is based will not work ... or does it? or is it better to create a new PR (that seems like a crufty way to go - you'd lose PR commentary). or should I merge the upstream branch into my feature branch and push that? basically: help me out with the correct workflow please! |
|
or should I merge the upstream branch into my feature branch and push that? <-- |
|
@durango took me a moment to figure out you were not asking a question but pointing out what I should do! I merged warning: possible EventEmitter memory leak detected. 11 listeners added. Use emitter.setMaxListeners() to increase limit.
Trace
at process.EventEmitter.addListener (events.js:160:15)
at process.on.process.addListener (node.js:768:26)
at new module.exports.ConnectorManager (/work/forks/sequelize/lib/dialects/mysql/connector-manager.js:122:13)
at new module.exports.Sequelize (/work/forks/sequelize/lib/sequelize.js:106:30)
at Object.module.exports.createSequelizeInstance (/work/forks/sequelize/spec/buster-helpers.js:44:12)
at Object.module.exports.initTests (/work/forks/sequelize/spec/buster-helpers.js:11:26)
at Object.<anonymous> (/work/forks/sequelize/spec/dao.spec.js:14:13)
at asyncFunction (/work/forks/sequelize/node_modules/buster/node_modules/buster-test/lib/buster-test/test-runner.js:189:16)
at callAndWait (/work/forks/sequelize/node_modules/buster/node_modules/buster-test/lib/buster-test/test-runner.js:221:23)
at next (/work/forks/sequelize/node_modules/buster/node_modules/buster-test/lib/buster-test/test-runner.js:241:31)I get a number of these - I don't think they are anything to do with my changes - is there known to be stuff 'broken' in secondly I get the following error: Error: [POSTGRES] HasMany (N:M) adds three items to the query chainer when calling sync
Error: Cannot find module '/work/forks/sequelize/node_modules/pg/lib/native/../../build/default/binding'
at Function.Module._resolveFilename (module.js:338:15)
at Function.Module._load (module.js:280:25)
at Module.require (module.js:364:17)
at require (module.js:380:17)
at Object.<anonymous> (./node_modules/pg/lib/native/index.js:12:12)
at Module._compile (module.js:456:26)
at Object.Module._extensions..js (module.js:474:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Module.require (module.js:364:17)how do I get rid of that?? other than this all tests are passing, a bit of help getting the above cleaned up so I can push the changes would be appreciated. |
|
_loose thought:_ projects such as https://github.com/defunkt/github-gem are an attempt to make that a workable proposition. |
|
The first error is to be expected, no problem in that The second one seems to be with the native pg binding, there seems to have |
|
@janmeier - hi, thanks that fixes the problem ... apparently I had to uninstall/install sqlite3 ... not sure exactly why, I guess I'm still learning to crawl when it comes to node/npm/et-al ... was this problem related to updates in the actual sequelize code base or is it more likely to be a result of a change in the version of node I am running (I vaguely remember using I have just pushed the results of merging in |
|
@durango i like this PR, however i was discussing the future data access API with sdepold. |
|
@mickhansen - hi, I am interested if you guys have any idea whether this PR will be accepted as it is or if you intend to strip out the interesting bits and reuse them in your own reimaging of the data access API ... and whether I can do anything to help? |
|
@iamjochem Lots of stuff going on for me currently so will be a while before i can implement the proposed/decided API. So i say we merge this and provide the functionality untill we can refactor the internals. |
|
@iamjochem You can help me in designing the new API when i get to it :) Would love your input when i get there. |
|
@mickhansen & co - sure I'd love to help if and where I can ... just give me a poke when the time is ripe. if you do go ahead and merge this then I think it would be good to tweak the PR slightly ... currently custom property getters and setters are defined via the options are in the model ... which I think is handy because it makes model composition more 'natural' (in my project I have 'meta objects' that specify what my sequelize models should look like, including shared/reusable stuff, that gets read by a 'builder' that composes, defines & associates the actual models) ... that is all well and god but I notice that someone suggested something like the following: sequelize.define('User',
{
firstname: {
type: Sequelize.STRING,
get: function() { /* magic here */ },
set: function() { /* magic here */ }
}
}, {/*options*/})i.e. place the getters/setters in the property definition itself ... If you agree to merge I would like to implement that on top of current functionality I have (in such a way that getters/setters given in the actual property definition take preference to any getters/setters that may be passed in, for said property, in the model what this would give is this:
what do you think? |
|
@iamjochem's loose thought: But the tool you linked seems to do a lot of the tiresome work of adding a new remote and checking out the right branch just to merge a single PR. The github gem seems to be abandoned though, and does not work properly on windows. Instead i used https://github.com/defunkt/hub , and it worked fine in merging #589. It was as simple as Thanks a lot showing me a new cool tool! :) |
|
@iamjochem I love your last example the most, it definitely feels more natural in my opinion (although the other way is useful for creating "virtual" fields as well). |
|
@durango - that last idea is a fairly small addition me thinks - I'll get right on it. |
…data attribute definition (by defining a "get" and/or "set" property) ... these take preference to getters/setters defined (for an attribute of the given name) in the options object - some buster tests included
…hope there are no timeouts in the postgres test run this time :-/
|
@durango, @mickhansen, @janmeier, @sdepold - I have updated the PR to include wat I suggested above ... I believe there is a big green button up top that would love a click ;-) |
|
ping - any change of actually merging this? |
|
awesome!!! very happy about this, means I can go back to using stock sequelize in my project rather than a fork :-) |
|
Hi, updated from 1.6 to 1.7 to play with this, works like a charm. |
|
hi @wonderwhy-er ... so-called 'pseudo-properties' defined by way of a getter are indeed not included in the I think you're best off redefining toJSON : function() {
return _.extend(this.values, {fakeprop_one: this.fakepropone, another_fakeprop: this.another_fakeprop });
}maybe there should be a standard mechanism for retrieving all data including the 'fake properties', regardless I don't think that the values of the fake properties should be included by default. (that said I have reservations regarding simply exposing the complete contents of a model instance via an API, I'm more inclined to explicitly define which data properties should be made available via API than just exposing all properties by using |
|
@iamjochem thanks for quick reply. I actually seen this hack in that transient properties thread mentioned before. Also, my problem actually is two fold as I also need to "remove" one of the values from toJSON result. Ended up writing something like this: instanceMethods: {
hidden_values: ['password'],
toJSON: function()
{
var res = this.values;
for(var getter_name in User.options.getterMethods)
{
res[getter_name] = this[getter_name];
}
for (var i = 0; i < this.hidden_values.length; i++)
{
var value_name = this.hidden_values[i];
delete res[value_name];
}
return res;
}
}As you see I also prefer for user to be able to choose. I was just asking if there may be is a better way. |
|
when I was writing my reply I was contemplating on adding an example (specifically with a User model and it's password field)! you might consider simply defining and using some app specific method (e.g. one thing worth checking is whether you need to clone the object that |
|
@iamjochem yeah user is most obvious case for this :) Ok I actually changed a little how I do this sequelize = new Sequelize(config.name, config.username, config.pass, {
define: {
instanceMethods: {
toJSON: function ()
{
var res = this.values;
console.log(this);
for (var getter_name in this.__options.getterMethods)
{
res[getter_name] = this[getter_name];
}
if(this.hidden_values)
{
for (var i = 0; i < this.hidden_values.length; i++)
{
var value_name = this.hidden_values[i];
delete res[value_name];
}
}
return res;
}
}
}
});Now its less of a problem I guess as its in one place. So at the moment in my tests it works fine. |
|
I am trying out the 1.7 alpha and had some questions about this specific issue. As per sdepolds initial comment, this removes, by default, the ability to access properties directly on a model (eg no more 'instance.foo'). So basically, doesn't this break backwards compatibility with any code that used sequelize before this update? If so, it seems like this shouldn't be in a minor update, right? Seems like a default getter should be defined to keep that backwards compatibility. Or am I missing something here/having a bug on my end? |
|
@greghart - see here for |
|
@iamjochem Great, that's what I was hoping. I wasn't seeing it but with verification that it should work I managed to track down the bug causing me to believe it was not working. I doubt it's a valid use case, but this does mean you can no longer build instances with other instances fed straight as the first argument. May sound like an odd use case, but I had to juggle two different Model's, one for a read only database, so when switching the connection to use for persistence I would do something like: instance = Instance.find(1).complete(...)
// This no longer works, but I'm not sure if this is proper anyways
instance2 = InstanceReadOnly.build(instance, {isNewRecord:false})
// Easy fix
instance2 = InstanceReadOnly.build(instance.values, {isNewRecord:false})In retrospect, I believe using instance.values was probably the more proper way to do this anyways. |

inspired by #70 and #373 (I repurposed some tests in #73, thanks to the author) I decided to have a stab at implementing getters and setters for DOA instances.
with this patch values for attributes defined in the Model are no longer stored as properties of DAO instances but rather they are stored in DAO.dataValues (with expection of 'association' attribute values, those still exist as plain properties on the DAO object, partly because I didn't want to f*** with the Association logic and partly because the values related to Association coupling are 'keys' and as such wrapping them in getters/setters seems a little bogus (why would you like to silently mutate them when setting/getting such key values).
all existing (and new) tests pass on the changes I made. (tests were added to 'buster' tests - yeehaw)
I still consider this work-in-progress but I'd love some feedback in the hope that this could eventually be merged (rather than die slowly at the bottom of the PR 'queue' ;-).
please ask questions, poke holes, etc!