forked from Satyam/satyam.github.com
-
Notifications
You must be signed in to change notification settings - Fork 0
/
WhyGalleryModel.html
343 lines (343 loc) · 17.3 KB
/
WhyGalleryModel.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8">
<TITLE>Satyam's GalleryModel</TITLE>
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.3.1/build/reset-fonts-grids/reset-fonts-grids.css">
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.3.1/build/base/base-min.css">
<STYLE TYPE="text/css">
PRE {
background-color: #e0e0e0;
border: 1px solid silver;
padding: 1em ;
font-size:85%;
margin:1em
}
code {
background-color:#e0e0e0;
padding:0 3pt;
}
</STYLE>
</HEAD>
<body class="yui-skin-sam">
<div id="doc" class="yui-t1">
<H1>Satyam's GalleryModel model</H1>
<P>After stumbling over a few issues with the standard YUI Model
component and adding patch over patch to the standard one, I found
out that the patches were so large as to make the patched files
irrelevant so I decided to release a separate version as a YUI
Gallery component. Though GalleryModel is still compatible with View,
it is not compatible with ModelList. At a later stage I expect to
include a subclass to ModelList that can accept either Model or
GalleryModel instances though, as I'll show later, it might be
unnecessary.</P>
<H2>Why a separate Model?</H2>
<P>Though Model works nicely in new applications, it is not easy to
use when dealing with legacy data fetched from existing SQL
databases. There are several features quite standard in existing
databases that it does not support, but it is one crucial detail that
made GalleryModel incompatible with Model which is that mixing both
data fields and information about those fields as Attributes and thus
in the same namespace puts artificial limits on the field names,
forcing unnecessary aliasing of existing names, and prevents us from
using attributes in our own models, for fear of colliding with field
names. Moreover, Model devotes several dozen lines to override
Attribute's <CODE>set</CODE> and <CODE>setAttrs</CODE>
methods, further proof that Attributes is not the right place for
data. Thus, the first order of business was to separate data storage
from other Model attributes and to provide methods to access that
data. GalleryModel uses <CODE>getValue</CODE>,
<CODE>setValue</CODE> and its plurals to access the
data. There is no need to define a data field before using it,
whatever is set will become part of the data in the field.</P>
<H3>Methods setValue and getValue</H3>
<P>Not using attributes for storing data fields does prevent us from
listening to its change events, using setters, getters or validators.
This, however, is well provided for by the <CODE>change</CODE>
method. The <CODE>setValue</CODE> method fires this
event and setting a before (<CODE>on</CODE>) listener
allows us to change the value finally set or even prevent its
setting.
</P>
<PRE>myModel.on('change', function (ev) {
switch (ev.name) {
// fieldOne will have its values doubled
case 'fieldOne':
ev.newVal *= 2;
break;
// fieldTwo will reject even values
case 'fieldTwo':
if (ev.newVal === Math.floor(ev.newVal / 2) * 2) {
ev.halt();
}
break;
}
});</PRE><P>
A centralized place to deal with changes in any of the fields is
quite handy. Contrary to regular attributes where the meaning or
purpose of each attribute might be totally different from that of
other attribute (for example, the <CODE>clientId</CODE>
attribute is defined by Model while the <CODE>initialized</CODE>
attribute comes from Base and they serve completely unrelated
purposes), all data fields are likely to be handled in a similar way
since they are all data. Moreover, the <CODE>setValues</CODE>
method fires the <CODE>change</CODE> event once for
the whole set of changes and then fires it once for each individual
field. This allows for validating values when they depend on one
another. If there is a <CODE>name</CODE> property in
the event façade, it is a single-field event and there will be
<CODE>prevVal</CODE> and <CODE>newVal</CODE>
properties with the current and new values for the named field. If
there is no <CODE>name</CODE>, there will be <CODE>prevVals</CODE>
and <CODE>newVals</CODE> (both plural) properties.
Thus, the simple validator/setter code above is actually valid even
in the case of setting multiple values at once. Care must be taken in
the default case not to reject <CODE>ev.name ===
undefined</CODE>, which would be the case in a call to <CODE>setValues</CODE>.</P>
<H3>isNew and isModified</H3>
<P>Having the attributes free for better users, I switched the <CODE>isNew</CODE>
and <CODE>isModified</CODE> methods to read-only
attributes. As I mentioned, I wanted to separate data from the
information about that data, the data being handled by <CODE>setValue</CODE>
and <CODE>getValue</CODE> and the information about
it via attributes. Thus, it made sense that <CODE>isNew</CODE>
and <CODE>isModified</CODE> became attributes, so
that they can also benefit from the change events fired on all
attributes. Additionally, those same attributes can be used to query
the status of each individual field by using the field name as a
sub-attribute. Thus, <CODE>myModel.get('isNew')</CODE>
gives the status of the whole model while
<CODE>myModel.get('isNew.fieldOne')</CODE> returns
the status of field <CODE>fieldOne</CODE>.</P>
<P>The <CODE>isNew</CODE> attribute does not rely on
the existence of a value for the primary key field, a dangerous
assumption in many cases. For example, a record using the Social
Security Number (SSN) as a primary key will ask the operator for its
value and thus would not be <I>undefined</I> even
for a newly created record. The standard Model would report
such a record as not being new and call <CODE>sync()</CODE>
with action set to <CODE>'update'</CODE> instead of
<CODE>'create'</CODE> as it should for a new record.
</P>
<H3>primaryKeys</H3>
<P>GalleryModel makes no assumptions upon what the name of the
primary key should be, or whether there are any, one or many. It
provides a <CODE>primaryKeys</CODE> attribute which,
as it name implies, assumes multiple-field primary keys. It defaults
to no primary keys and can be set to a string containing the name of
the primary key field, or an array of field names. Following on the
previous example, the SSN is just one of many documents that can be
used to identify a person, thus, many tables use a combination of
document type (SSN, passport, drivers license) and document
number/value as a primary key.
</P>
<P>The <CODE>primaryKeys</CODE> attribute will always
return an array and it defaults to an empty array meaning no primary
keys at all. As with <CODE>isNew</CODE> and
<CODE>isModified</CODE>, using a field name as a
sub-attribute allows us to ask whether a particular field is a
primary key. Thus, <CODE>myModel.get('primaryKeys')
</CODE>might return <CODE>['docType', 'doc']</CODE>
while <CODE>myModel.get('primaryKeys.firstName')</CODE>
would then return <I>false</I>, since <CODE>firstName</CODE>
is not part of the primary key for that model.</P>
<P>Primary key field values cannot be changed, <CODE>setValue</CODE>
will fail to set a primary key field, unless <I>undefined</I>; once
set, it won't change.
</P>
<P>Method <CODE>getPKValues</CODE> returns an object
with the field names and values of the fields that make the primary
key of the record.
</P>
<P><CODE>primaryKeys</CODE> can only be set once.</P>
<H3>getValues and getChangedValues</H3>
<P>Method getValues returns an object with the field names and values
of all the data fields in the model, such as:</P>
<PRE>{docType: 'SSN', doc:'123-456-7890', firstName:'John', lastName:'Doe'}</PRE>
<P>Method <CODE>getChangedValues</CODE> returns only
those fields that have changed since loaded from or last saved to the
remote source, in other words, since last synched against the remote
source. For each field it returns an object containing <CODE>prevVal</CODE>
and <CODE>newVal</CODE> properties.
</P>
<PRE>{firstName: {prevVal:'John', newVal:'Jeff'}}</PRE>
<P>The previous value is useful for shared databases where multiple
users might be trying to change the same record. A simple way to
prevent it is to include the previous value of the field to be
changed as part of the UPDATE statement so that the change will fail
if the affected field has been changed since last read, for example:</P>
<PRE>UPDATE stock SET qty=3 WHERE prodID = '123456' and qty=5</PRE>
<P>If no records are affected by the SQL statement above it means
that the item is no longer in the database or, more likely, that the
quantity has already been changed by some other user and there are no
longer 5 units in stock. (from a database point of view there are far
better ways to solve this issue, but as an example it clearly shows
the problem).</P>
<P>Since primary key fields can never change, they will never be
returned by <CODE>getChangedValues</CODE>.</P>
<H3>Method toJSON</H3>
<P>Model uses method <CODE>toJSON</CODE> to return
the values of all data fields. In GalleryModel you use <CODE>getValues</CODE>
which is what <CODE>toJSON</CODE> defaults to.
However, GalleryModel expects you would redefine <CODE>toJSON</CODE>
to suit whatever format your remote source expects while <CODE>getValues</CODE>
remains untouched.</P>
<P>Once again, Model mixes up two separate features. Just as it
assumes that if the single primary key field it provides is
<I>undefined</I>, the record must be new, which is not always the
case, here, it assumes <CODE>toJSON</CODE> to be the
means of obtaining the model raw values. This should not be so.
<CODE>toJSON</CODE> is more like <CODE>toString</CODE>,
a standard interface that other code can call. As with <CODE>toString</CODE>,
<CODE>toJSON</CODE> is meant to be redefined by the
implementor to return whatever it is he/she means to have serialized
into JSON, thus, it must be separated from <CODE>getValues</CODE>,
which should not be so freely redefined.</P>
<P>Initially, <CODE>toJSON</CODE> calls <CODE>getValues</CODE>,
however, it can be easily redefined like this:</P>
<PRE>toJSON: function() {
return {
keys: this.getPKValues(),
values: this.getChangedValues()
};
}</PRE><P>
By the way, though the return value of <CODE>toJSON</CODE>
should be suitable to pass to <CODE>Y.JSON.stringify</CODE>,
toJSON needs not be called directly. You rarely call <CODE>toString</CODE>,
you safely assume that <CODE>toString</CODE> will be
called if a string representation of the object is needed. Likewise,
you do:</P>
<PRE>Y.JSON.stringify(myModel)</PRE>
<P>and not
</P>
<PRE><STRIKE>Y.JSON.stringify(myModel.toJSON())</STRIKE></PRE>
<P>It is the JSON serializer the one that will go and look for a
<CODE>toJSON</CODE> method and use it if any exists.
</P>
<H3>The loaded and saved event</H3>
<P>The first change is somewhat semantic, these events signal
something that has already occurred, the remote operation has already
happened with any side effects it might have had; hence they have
been renamed to their past tense. There are no other changes to the
<CODE>saved</CODE> event. However, there are two
changes to the <CODE>loaded</CODE> event.
</P>
<P>First, the <CODE>loaded</CODE> event may fire in a
call to <CODE>save</CODE> and after the <CODE>saved</CODE>
event if the response from the server contains any data, which is
indicated by the <CODE>parser</CODE> method returning
a non-<I>falsy</I> value. This allows to consistently intercept all
values loaded, be it from explicitly calling <CODE>load</CODE>
or as a side effect of saving.</P>
<P>Second, the <CODE>loaded</CODE> event is
preventable, so that the values loaded can be ignored. This would be
most unusual in a <CODE>load</CODE> operation but can
be useful if it happens in response to a call to the <CODE>save</CODE>
method. The event façade to <CODE>loaded</CODE>
provides the <CODE>action</CODE> property to tell
them apart. Preventing the <CODE>loaded</CODE> event
when fired by a <CODE>save</CODE> operation does not
affect the saving nor the firing of the <CODE>saved</CODE>
event since both had already happened.</P>
<H2>Extensions</H2>
<P>The GalleryModel component is provided with several extensions
that can augment a subclass derived from GalleryModel by using the
following code:</P>
<PRE>var myModel = Y.Base.create('my-model', Y.GalleryModel,
[Y.GalleryModelChronologicalUndo, Y.GalleryModelMultiRecord],
// …
)
</PRE><H3>
Undo</H3>
<P>Two separate undo extensions are included, the simplest
Y.GalleryModelSimpleUndo returns any field or all fields to its
previous value. It has only one level of undo per field. The <CODE>undo</CODE>
method accepts the name of the field to be undone; if not specified,
all fields will be undone, whatever that might mean. Since different
fields might have been modified a different number of times, applying
one level of undo to all of them will result in possibly inconsistent
states.</P>
<P>The Y.GalleryModelChronologicalUndo provides a full, though
possibly costly, undo facility. It will apply undoes in strict
reverse chronological order up to the last load or save of the
record. Multi-field changes (using <CODE>setValues</CODE>)
will be undone in a single step. Changes saved to the remote source
cannot be undone. The undo stack is reset on any synching to the
remote source. To undo all changes since the record was synched,
GalleryModel provides the <CODE>reset</CODE> method,
which will do it in just one step.</P>
<A name="MultipleRecords"></A><H3>Multiple Records</H3>
<P>The Y.GalleryModelMultiRecord extension allows a single instance
of GalleryModel to store multiple records using the Flyweight
pattern. Records will be stored in shelves and only one at a time is
exposed as a normal record would. To change the record being exposed,
the <CODE>index</CODE> attribute allows moving about
the shelves.
</P>
<P>This extension also captures the <CODE>loaded</CODE>
event so that if the parsed response to a load from the server is an
array, all its items will be added to the shelves. Upon loading, the
last record loaded will be the current. The shelves are not emptied
before loading, call the <CODE>empty</CODE> method to
start from zero. If the response contains a single item, only the
exposed record is affected.
</P>
<P>Methods <CODE>each</CODE> and <CODE>eachModified</CODE>
are provided to loop through the records. The latter is faster than
using <CODE>each</CODE> and then checking the
<CODE>isModified</CODE> attribute. The function will
receive the index of the record being shown and executes in the
context of the model instance. Since shelving a record takes time, if
the callback function has not modified the record, returning exactly
<I>false</I> will save that time.
</P>
<P>The Y.GalleryModelSortedMultiRecord further enhances the
multi-record model by ensuring that it is always kept sorted as
defined by the <CODE>sortField</CODE> and <CODE>sortDir</CODE>
attributes. The <CODE>sortField</CODE> attribute is
normally the name of a field to sort the records by. The <CODE>sortDir</CODE>
attribute can be either <CODE>'asc'</CODE> or <CODE>'desc'</CODE>.
Whenever either of these attributes is changed the records will be
re-sorted. New records are placed in their proper order
automatically.
</P>
<P>The <CODE>sortField</CODE> attribute can also be
set to a function. This function will receive an object with the
values for the record to be sorted and it should return a string with
the value or values of the field or fields to be compared to
determine the sorted order. This allows for multiple field sorting as
well as to process the values in any way needed before comparing
them. The function should not alter the object received since, in
order to improve performance, what it gets is the original, not a
clone. The <CODE>sortField</CODE> function should not
expect <CODE>this</CODE> to contain any valid value.</P>
<P>Existing records will also be re-arranged when setting a value if
the name of the field changed is the same as that set in <CODE>sortField</CODE>.
If <CODE>sortField</CODE> contains a function a new
position for the record will always be recalculated since it cannot
know a priori that the record need not move.</P>
<P>The <CODE>find</CODE> methods locates a record by
the value of the field named in <CODE>sortField</CODE>
only if it is a string and not a function. It will return the index
of the record found or <EM>null</EM> if not. By default it will make
the found record current unless asked not to by setting the second
argument to <EM>false</EM>. If there are several records with the
same value for the field, it may return any of them.</P>
<H3>Conclusion</H3>
<P>GalleryModel solves several deficiencies the standard Model has,
specially as related to existing database tables. Data fields and
information about them is now clearly separate and not all mixed up
into regular YUI attributes. Unfortunately this has meant making it
incompatible with Y.Model and, though Y.View can work with
GalleryModel since it never made any assumptions about Y.Model;
Y.ModelList, however, needs to be changed and though I plan to do it
eventually, I am not sure it will be needed. With the
multiple-record extension, there might not be a need for many of the
functions provided by ModelList. ModelList might still be used to
gather instances of models from different subclasses, that is, that
don't represent occurrences of the same type of record, which can be
better represented by a multi-record GalleryModel.
</P>
</div>
</BODY>
</HTML>