@sap/cds 1.15.1 → 1.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/TUTORIAL.md DELETED
@@ -1,1236 +0,0 @@
1
- node-cds: Core Data Services for node.js
2
- ========================================
3
-
4
- Important note:
5
- ---------------
6
- The node-cds library is now considered feature complete.
7
- It will remain fully supported but will not receive further
8
- enhancements in future releases.
9
-
10
-
11
- Entity Declarations
12
- -------------------
13
-
14
- The data declaration part of your JavaScript file defines the entity
15
- types based on existing CDS and/or HANA tables and views:
16
-
17
- cds.$importEntities([
18
- // for CDS:
19
- { $entity: "cds.namespace::cds_context.cds_entity" },
20
- // for plain SQL:
21
- { $schema: "SCHEMA", $table: "package::TABLE"), $name: "name" }
22
- ], function (error, entities) { … });
23
-
24
- For single entity imports the `$importEntity` function may also be
25
- used.
26
-
27
- Both import methods pass a dictionary of imported entities indexed by
28
- entity name to the callback function. This dictionary comprises both
29
- explicitly and implicitly imported entities, including previously
30
- imported entities if required.
31
-
32
- cds.$importEntities([{
33
- $entity: "sap.hana.xs2.cds.examples.data::bboard.post"
34
- }], function (error, entities) {
35
- var post = entities["sap.hana.xs2.cds.examples.data::bboard.post"];
36
- var user = entities["sap.hana.xs2.cds.examples.data::bboard.user"];
37
- // automatically imported because of association from Post to User
38
- });
39
-
40
- For most CDS data models and simple SQL data models this is all that
41
- is required to define an entity.
42
-
43
- Advanced options allow you to override certain properties of the
44
- columns to import. In the simplest case, you simply want to rename a
45
- table column for your JavaScript object property:
46
-
47
- cds.$importEntities([
48
- { $entity: "sap.hana.xs2.cds.examples.data::bboard.post",
49
- $fields: {
50
- PostedOn: { $column: "Created" }
51
- }
52
- }
53
- ], callback);
54
-
55
- Primary key information is automatically imported, but you may also
56
- add or change key information, e.g., for CDS entities defined with the
57
- `@nokey` attribute:
58
-
59
- cds.$importEntities([{
60
- $entity: "sap.hana.xs2.cds.examples.data::bboard.post",
61
- $fields: {
62
- pid: { $key: true }
63
- }
64
- }], callback);
65
-
66
- You may also provide an existing HDB sequence to generate keys
67
- automatically for you. By using a sequence key fields may be
68
- populated automatically by the database.
69
-
70
- cds.$importEntities([{
71
- $entity: "sap.hana.xs2.cds.examples.data::bboard.user",
72
- $fields: {
73
- uid: { $key: "\"sap.hana.xs2.cds.examples.data::bb_keyid\"" }
74
- }
75
- }], callback);
76
-
77
- Note that node-cds does not apply any additional logic to key
78
- generation. It is left to the application and the sequence to
79
- generate valid, unique keys for your entities.
80
-
81
- `$importEntities()` passes a dictionary of entity objects for the
82
- entity types defined to its callback. If you need to access an entity
83
- in a different scope you can use the `$getEntity()` function to
84
- retrieve the entity object by entity name:
85
-
86
- function foo() {
87
- cds.$importEntity({ $entity: "cds_namespace::cds_context.cds_entity" }),
88
- callback);
89
- }
90
-
91
- function bar() {
92
- cds.$getEntity("cds_namespace::cds_context.cds_entity"),
93
- function(err, entity) { … }
94
- }
95
-
96
- The `$getEntity` function not only saves you from storing your
97
- entities in global variables but also offers flow control to ensure
98
- that the entity and all its dependencies have been imported
99
- completely. Note that the callback of a `$getEntity` call for an
100
- entity that is not imported will never be called!
101
-
102
- There is also a synchronous version `$getEntitySync` that will return
103
- the entity immediately or null if the import has not been completed
104
- yet.
105
-
106
- var someEntity = cds.$getEntitySync("cds_namespace::cds_context.cds_entity");
107
- if (!someEntity)
108
- console.warn("well, now what?!");
109
-
110
- Finally we would like to stress that node-cds is a consumption-only
111
- framework that will only import externally defined data models. It is
112
- beyond the scope of node-cds to create tables and modify existing CDS
113
- data models permanently. This scope definition will not change in the
114
- future.
115
-
116
-
117
- Associations
118
- ------------
119
-
120
- One of the benefits of the semantically rich data models of CDS is the
121
- ability to handle associations conveniently. node-cds supports plain
122
- one-to-one, one-to-many, and many-to-many CDS associations in all of
123
- their flavors.
124
-
125
- While the CDS core implementation for managed one-to-many and
126
- many-to-many associations is still incomplete node-cds already
127
- supports these types of associations in a way that makes it easy to
128
- migrate once they become available in CDS as well.
129
-
130
- CDS one-to-one (and one-to-zero-or-one) associations are imported
131
- automatically. To define one-to-one associations manually you provide
132
- an `$association` property that specifies the associated entity type
133
- and the foreign key that links the parent entity to the associated
134
- target entity:
135
-
136
- cds.$importEntity({
137
- $entity: "sap.hana.xs2.cds.examples.data::bboard.post",
138
- $fields: {
139
- PostedBy: {
140
- $association: { $entity: "sap.hana.xs2.cds.examples.data::bboard.user" },
141
- uid: { $column: "Author" }
142
- }
143
- }
144
- }, callback);
145
-
146
- In this example the link between Post and User is defined through the
147
- foreign key relation so that `USERS.uid = POSTS.Author` holds. Note
148
- that the `Author` column of table `POSTS` is not mapped to a field of
149
- the `Post` entity; it’s used exclusively for following the
150
- association. The foreign key may reference any number of columns of
151
- the underlying database table.
152
-
153
- Entities referenced by name are imported automatically with default
154
- options if no matching `$importEntities()` call can be found.
155
-
156
-
157
- ### One-to-Many Associations
158
-
159
- The CDS specification defines one-to-many associations by using
160
- backlinks from the targets to the parent. Assume for our example that
161
- each post may have any number of unique comments, where each comment
162
- contains a reference to its parent post:
163
-
164
- entity comment {
165
- key cid: Integer not null;
166
- Text: text;
167
- Author: association [1] to user;
168
- Parent: association [1] to post; // as backlink
169
- Created: UTCDateTime;
170
- };
171
-
172
- Since the CDS implementation does not support backlinks yet, we cannot
173
- add comments to our CDS definition of post, but we may augment the
174
- `Post` data model in our import:
175
-
176
- cds.$importEntity({
177
- $entity: "sap.hana.xs2.cds.examples.data::bboard.post",
178
- $fields: {
179
- Comments: {
180
- $association: {
181
- $entity: "sap.hana.xs2.cds.examples.data::bboard.comment",
182
- $viaBacklink: "Parent"
183
- }
184
- }
185
- }
186
- }, callback);
187
-
188
- This is equivalent to importing the following, still unsupported CDS
189
- definition:
190
-
191
- entity post {
192
- key pid: Integer not null;
193
- ...
194
- Comments: association [0..*] to comment via backlink Parent;
195
- };
196
-
197
- Once CDS support for backlinks is implemented, the node-cds import
198
- above simplifies to
199
-
200
- cds.$importEntity({ $entity: "sap.hana.xs2.cds.examples.data::bboard.post" },
201
- callback);
202
-
203
- Either way, the comments will appear as an array in our instances of
204
- the Post entity:
205
-
206
- tx.$get(Post, { pid: 69 }, function(error, post) {
207
- for (var i = 0; i < post.Comments.length; ++i) {
208
- if (post.Comments[i].Author.Name === "Alice") { … }
209
- });
210
-
211
- Note that the association array should be seen as a read-only array
212
- that gathers the association targets for convenience. While
213
- individual array elements may be manipulated directly their membership
214
- to the association and thus the array is governed by their backlink
215
- property only. This has some potentially unexpected consequences when
216
- saving instances with backlink associations that are further detailed
217
- in the section “Working with entities”.
218
-
219
-
220
- ### Many-to-Many Associations
221
-
222
- The CDS specification defines many-to-many associations by using
223
- linking entities that connect parent and target instances. Assume for
224
- our example that any number of tags may be attached to our posts:
225
-
226
- entity tag {
227
- key tid: Integer not null;
228
- Name: String(63);
229
- };
230
-
231
- The actual linkage data is stored in a separate table. For plain SQL,
232
- this table may be specified directly, but for CDS we assume a
233
- corresponding entity definition:
234
-
235
- @nokey entity tags_by_post {
236
- lid: Integer;
237
- LinkTag: association to tag;
238
- LinkPost: association to post;
239
- };
240
-
241
- Since the CDS implementation does not support linking entities yet, we
242
- can manually augment our data model in the import statement:
243
-
244
- cds.$importEntity({
245
- $entity: "sap.hana.xs2.cds.examples.data::bboard.post",
246
- $fields: {
247
- Tags: {
248
- $association: {
249
- $entity: "sap.hana.xs2.cds.examples.data::bboard.tag",
250
- $viaEntity: "sap.hana.xs2.cds.examples.data::bboard.tags_by_post",
251
- $source: "LinkPost",
252
- $target: "LinkTag"
253
- }
254
- }
255
- }
256
- }, callback);
257
-
258
- The `$source` and `$target` properties indicate the direction of the
259
- association. This is strictly required if source and target refer to
260
- the same entity; for simplicity, however, we enforce that these
261
- properties are always supplied.
262
-
263
- Above annotation is equivalent to importing the following, still
264
- unsupported CDS definition:
265
-
266
- entity post {
267
- key pid: Integer not null;
268
- ...
269
- Tags: association [0..*] to ds_comments via entity tags_by_post;
270
- };
271
-
272
- Once CDS support for backlinks is implemented, the node-cds import
273
- simplifies to
274
-
275
- cds.$importEntity({ $entity: "sap.hana.xs2.cds.examples.data::bboard.post" },
276
- callback);
277
-
278
- Note again that an explicit import of `Tags` or `Tags_by_post` is not
279
- required. The linking entities such as `Tags_by_post` may or may not
280
- have a key, and they could have additional fields besides the
281
- `$source` and `$target` fields.
282
-
283
- The tags will appear as a native JavaScript array in our instances of
284
- the `Post` entity:
285
-
286
- tx.$get(Post, { pid: 69 }, function (error, post) {
287
- tx.$find(Tag, { Text: "cool" }, function (error, tags) {
288
- if (post.Tags.indexOf(tags[0]) < 0) {
289
- // ...
290
- }
291
- }
292
- });
293
-
294
- Via entity association arrays behave differently from via backlink
295
- association arrays in that element membership is controlled directly
296
- through the array. In fact, direct manipulation of linking entities
297
- such as `Tags_by_post` is discouraged, although it is possible. For
298
- further details see the section "Working with entities".
299
-
300
-
301
- ### Unmanaged Associations
302
-
303
- The most general form of associations supported by CDS are unmanaged
304
- associations that define an arbitrary `JOIN ... ON` condition:
305
-
306
- entity toppost {
307
- key pid: Integer not null;
308
- ...
309
- TopReplies: association to Post on TopReplies.Parent.pid = pid and
310
- TopReplies.Rating >= 3;
311
- };
312
-
313
- Unmanaged associations are imported automatically and need not be
314
- supplied to the import call:
315
-
316
- cds.$importEntity({ $entity: "sap.hana.xs2.cds.examples.data::bboard.topposts" },
317
- callback);
318
-
319
- Similarly to backlink associations, unmanaged target instances are
320
- stored in a conceptually read-only array. Due to the unmanaged nature
321
- of these associations the contents of the array cannot reflect unsaved
322
- changes to the relation and must be refreshed manually.
323
-
324
-
325
- Working With Managed Entities
326
- -----------------------------
327
-
328
- Once we have imported all our entities into our application we can use
329
- the resulting handles to work with their instances.
330
-
331
- For this node-cds provides two different modes to interact with the
332
- database: managed mode and unmanaged mode. Managed mode works with
333
- entity instances, i.e., singleton objects with “personality” that
334
- correspond one-to-one to database records. Unmanaged mode, on the
335
- other hand, works with plain values that are simple, flat JavaScript
336
- object values.
337
-
338
- In this tutorial we will start off with managed mode and cover
339
- unmanaged mode in the corresponding section below.
340
-
341
- To retrieve an existing entity instance in managed mode, we may query
342
- it by its key:
343
-
344
- tx.$get(Post, { pid: 1 }, function (error, post) {
345
- if (error)
346
- console.error(error)
347
- else
348
- notify(post.Author.Email);
349
- });
350
-
351
- This will retrieve the post with `pid == 1` if it exists, or null
352
- otherwise. Note that specifying a non-existing key is not an error
353
- condition. The $get() method requires that all key properties be
354
- supplied.
355
-
356
- The more general `$find()` method will search for instances that match
357
- the given property conditions:
358
-
359
- tx.$find(User, { Name: "Alice" }, function (error, users) {
360
- console.log("Number of users called Alice: " + users.length);
361
- });
362
-
363
- The instance filter expression may be built using the following basic
364
- expression language that is valid for all JavaScript and SQL types:
365
-
366
- <expr> ::= { <cond>, <cond>, … }
367
- <cond> ::= prop: value | prop: { <op>: value, … }
368
- <op> ::= $eq | $ne | $lt | $le | $gt | $ge | $like | $unlike | $null
369
-
370
- All conditions in an expression are joined by logical-AND. The
371
- expression `{ prop: value }` is a shorthand for `{ prop: { $eq: value
372
- } }`.
373
-
374
- The comparison operators `$eq`, `$ne`, `$lt`, `$le`, `$gt`, `$ge`
375
- apply to all suitable types if the value is supplied in appropriate
376
- format:
377
-
378
- tx.$find(E, { p: { $lt: 100 } }); // returns all instances where p < 100
379
- tx.$find(E, { p: { $ge: 100, $le: 200 } }); // … p between 100 and 200
380
- tx.$find(E, { p: "Bob", q: { $ne: "Jones" } }); // … p is Bob but q is not Jones
381
-
382
- The `$like` and `$unlike` operators can be used for SQL-like pattern
383
- matching:
384
-
385
- tx.$find(User, { Name: { $like: "B%" } }); // returns all users whose name starts with B
386
- tx.$find(User, { Name: { $unlike: "J.." } }); // returns Bill but not Jim
387
-
388
- The `$null` operator checks if a given property is `NULL` in the database:
389
-
390
- tx.$find(Post, { Rating: { $null: true } }); // returns posts with unknown rating
391
- tx.$find(Post, { Created: { $null: false } }); // returns posts with known authors
392
-
393
- The `$empty` operator checks if an association is missing or empty:
394
-
395
- tx.$find(Post, { Parent: { $empty: true } }); // returns posts without parent
396
- tx.$find(Post, { Comments: { $empty: false } }); // returns posts with comments
397
-
398
- Expressions are evaluated by the database but also by JavaScript when
399
- checking the entity cache for matching instances (see section on
400
- Entity Management below). This works for simple types such as integers
401
- and strings, but requires user intervention for complex types, such as
402
- `DATE`, and for types that have no native JavaScript equivalent, such
403
- as `DECIMALS`.
404
-
405
- Handling those types meaningfully requires a comparison function
406
- with `$using` that matches the given implementation for non-native types.
407
-
408
- The `DATETIME` type, for example, is returned as string. To retrieve
409
- instances relative to a certain date the following code could be used:
410
-
411
- var datecomp = function(arg1, arg2) {
412
- return (+(new Date(arg1))) - (+(new Date(arg2)))
413
- };
414
-
415
- tx.$find(Entity, {
416
- datevalue: {
417
- $le: "2012-02-29T01:02:03.000Z",
418
- $using: datecomp }
419
- }, callback);
420
-
421
- Note that HANA does not store timezone information, so passing JavaScript
422
- `Date` objects to the query (including `$query()`) is likely to cause
423
- issues.
424
-
425
- node-cds also supports batch retrieval to reduce the number of
426
- required callback functions:
427
-
428
- tx.$getAll([
429
- { $entity: Post, pid: 1 },
430
- { $entity: User, uid: 2 }
431
- ], function (error, instances) {
432
- console.log("Post " + instances[0] + " was not made by user " + instances[1]);
433
- });
434
-
435
- If you abhor the mixing of entity type and entity key the $prepare
436
- auxiliary function also provides the required entity type information
437
- to `$getAll()`:
438
-
439
- tx.$getAll([
440
- Post.$prepare({ pid: 1 }),
441
- User.$prepare({ uid: 2 })
442
- ], function (error, instances) { … });
443
-
444
- Note that there is no `$findAll()` method, as `$find()` already
445
- returns a list of instances.
446
-
447
- Managed queries are inherently limited in their expressiveness, as the
448
- node-cds runtime needs to check its instance cache against the filter
449
- condition provided to `$find()`. Applications requiring advanced
450
- query capabilities should use unmanaged queries described in the next
451
- section. Of course, both managed and unmanaged queries can be used
452
- side by side.
453
-
454
-
455
- ### Updating Entities
456
-
457
- Entity instances are regular JavaScript objects and can be used and
458
- modified as one would expect:
459
-
460
- post.Ranking++;
461
- post.Author = users[0];
462
- post.Author.Name = "Newt";
463
-
464
- All changes are made in memory only. If we want to persist our
465
- changes for an instance we invoke the `$save()` method:
466
-
467
- tx.$save(post, function (error, result) {
468
- // post and its associated entity instances updated
469
- // ...
470
- });
471
-
472
- Calling `$save()` will flush our in-memory changes of that instance to
473
- the database, following all associations reachable from the root
474
- instance. Note that all instances will be updated, including
475
- instances not actually modified.
476
-
477
- If auto commit is active, then all changes written to the database are
478
- automatically committed. Without auto commit, you will have to call
479
- `tx.$commit()` and/or `tx.$rollback()` yourself in order to persist or
480
- undo your changes on the database.
481
-
482
- The result argument of the callback function receives the updated
483
- instance as written on the database. It is identical to the instance
484
- argument the `$save()` method was called on.
485
-
486
- As instance keys must not be changed, all key properties are locked
487
- and cannot be modified; attempts to do are silently ignored:
488
-
489
- var post = tx.$get(Post, { pid: 1 });
490
- post.pid = 69; // silently ignored
491
- console.log(JSON.stringigy(post)); // still pid == 1
492
-
493
- There is an additional batch persist for persisting multiple instances
494
- in one operation:
495
-
496
- tx.$saveAll([ post1, user2 ]); // persists post1 and user2 plus their dependencies
497
-
498
- Note that modified instances in memory always take precedence over
499
- their unmodified versions on the database. For more details and
500
- further information on some of the resulting subtleties please refer
501
- to the section on Entity Management and Consistency below.
502
-
503
-
504
- ### Creating new Entities
505
-
506
- New instances are created simply by saving new values with
507
- non-existing keys:
508
-
509
- tx.$find(User, { Name: "Alice" }, function (error, users) {
510
- var alice = users[0] || null;
511
- var post = {
512
- $entity: Post,
513
- id: 101,
514
- Title: "New Post", Text: "Hello BBoard!", Rating: 1, Created: new Date(),
515
- Author: alice
516
- };
517
- tx.$save(post, function (error) { … });
518
- });
519
-
520
- Since the original post object is not a `Post` instance yet, you’ll need
521
- to tell node-cds about its entity type by including a `$entity`
522
- property. Alternatively, you could use the trivial $prepare helper
523
- function:
524
-
525
- var post = { pid: … }; // no $entity: … property
526
- tx.$save(Post.$prepare(post), function (error) { … });
527
-
528
- New and existing instances may be mixed, as in the example
529
- above. node-cds tracks if an object represents an new or an existing
530
- instance and will use an `INSERT` or an `UPDATE` statements accordingly.
531
- Trying to save an existing instance with a value that was not
532
- retrieved from the database will yield a duplicate key exception from
533
- the database.
534
-
535
- var user = { $entity: User, uid: 42, Name: "Newt" };
536
- tx.$save(user, function(error) {
537
- // user #42 created
538
- });
539
-
540
- var update = { $entity: User, uid: 42, Name: "Ann Newt" }; // must use $get instead
541
- tx.$save(user, function(error) { … }); // duplicate key error
542
-
543
- Key properties for which an HDB sequence or an appropriate `$init`
544
- function has been supplied may be omitted from the entity call.
545
-
546
-
547
- ### Discarding Entities
548
-
549
- Retrieved entity instances are stored in the entity manager cache and
550
- subject to general JavaScript garbage collection. The `$discard()`
551
- method permanently deletes an instance from the database:
552
-
553
- tx.$get(Post, { pid: 99 }, function (error, post) {
554
- tx.$discard(post, function (error) { … });
555
- });
556
-
557
- The `$discard()` callback has a single argument for potential errors.
558
-
559
- Note that after calling `$discard()` on an instance, the actual
560
- JavaScript object remains in memory as an unmanaged entity instance,
561
- i.e., `$find()` will no longer not return references to it. It is up to
562
- the application to not use any remaining references to the object that
563
- may still be stored in some JavaScript variables.
564
-
565
- Note that `$discard()` cannot be used to delete instances based on a
566
- condition. To delete multiple instances without instantiating them
567
- before use the `$delete()` method described below.
568
-
569
- The `$discardAll()` method for batch discards works analogous to
570
- the `$saveAll()` method:
571
-
572
- tx.$discardAll([ post1, post2 ], function (error) {
573
- // ...
574
- }); // discards post1 and post2
575
-
576
- For the special use case of deleting instances on the database
577
- without instantiating them in memory first node-cds provides the
578
- `$delete()` operation for unmanaged deletes:
579
-
580
- tx.$delete(Post, { Rating: { $lt: 3 } }, function (err) {
581
- // deletes all posts where Rating < 3
582
- });
583
-
584
- An unmanaged delete will delete all matching records on the database,
585
- ignoring the state of the entity cache. Likewise, `$delete`s will not
586
- cascade to target instances. Thus, the set of affected entity instances
587
- may differ from that of the sequence
588
-
589
- tx.$findAll(Post, <cond>, function (error, posts) {
590
- tx.$discardAll(posts, function (error) { … });
591
- });
592
-
593
- In fact, `$delete()` is merely syntactic sugar for
594
- `$query().$matching().$delete()` (see below). Please use `$delete()` with
595
- care.
596
-
597
-
598
- ### Associations and Custom Types
599
-
600
- Both CDS types and CDS associations are supported and will yield
601
- nested structures in the resulting JavaScript entity. For example,
602
- the import of the CDS definitions
603
-
604
- entity user {
605
- key uid: Integer not null;
606
- Name: String(63) not null;
607
- };
608
-
609
- type text {
610
- Text: String(255);
611
- Lang: String(2);
612
- };
613
-
614
- entity post {
615
- key pid: Integer not null;
616
- Title: String(63) not null;
617
- Text: text;
618
- Author: association [1] to user;
619
- Created: UTCDateTime;
620
- };
621
-
622
- will yield a Post entity whose instances may look like
623
-
624
- {
625
- "pid": 102,
626
- "Title": "Re: First Post!",
627
- "Text": {
628
- "Lang": "EN",
629
- "Text": "You beat me to it."
630
- },
631
- "Author": {
632
- "uid": 2,
633
- "Name": "Bob"
634
- },
635
- "Created": "2014-04-25T09:11:30.000Z"
636
- }
637
-
638
- Overriding properties in such structured CDS entities works analogous
639
- to the flat case:
640
-
641
- cds.$importEntity({
642
- $entity: "sap.hana.xs2.cds.examples.data::bboard.post",
643
- $fields: {
644
- Text: { Lang: { $init: "EN" } }
645
- }
646
- }, callback);
647
-
648
-
649
- ### Lazy Navigation
650
-
651
- By default, all associations are eagerly resolved, i.e., their targets
652
- are included in the parent instance object. For heavily connected
653
- data this may lead to very large data structures in memory.
654
-
655
- To control which associations are being followed associations may be
656
- declared `$lazy`:
657
-
658
- cds.$importEntity({
659
- $entity: "sap.hana.xs2.cds.examples.data::bboard.post",
660
- $fields: {
661
- Parent: { $association: { $lazy: true } }
662
- },
663
- $name: "LazyPost"
664
- }, callback);
665
-
666
- A lazy association will delay the retrieval of the associated instance
667
- or instances until their property values are actually required:
668
-
669
- tx.$get(LazyPost, { pid: 1 }, function (error, post) {
670
- // retrieves single Post and Author from database
671
- post.Rating++;
672
- if (post.Author.Name === "Chris") {
673
- // now retrieve parent post from database, if it exists
674
- post.Parent.$load(function (error, parent) {
675
- if (parent)
676
- parent.Rating++;
677
- });
678
- }
679
- });
680
-
681
- The $load() function asynchronously retrieved that target instance or
682
- instances from the database and passes them to the callback function.
683
- Additionally, the target value is added to the association parent
684
- instance.
685
-
686
- Note that updates to an entity instance will not update
687
- associated lazy instances if they haven’t been followed yet!
688
-
689
- A lot may happen between the retrieval of some entity instance and the
690
- navigation to any of its lazy association targets. It is left to the
691
- application to ensure the consistency of data.
692
-
693
-
694
- Unmanaged Queries
695
- -----------------
696
-
697
- In unmanaged mode we work with plain structured values retrieved from
698
- HANA by arbitrary queries. Unlike in managed mode, these general
699
- queries support all of the advanced HANA functionality for retrieving
700
- data.
701
-
702
- A general query related to an entity is built by calling the `$query()`
703
- method of the entity function:
704
-
705
- var qPosts = Post.$query();
706
-
707
- The resulting object returned by `$query()` represents an initial query
708
- for all of the fields of the underlying entity, but without any
709
- associated entities.
710
-
711
- Queries may be built from existing queries by chaining `$where`,
712
- `$matching`, `$project`, and further operators. With these operators,
713
- results can be constructed incrementally without accessing the
714
- database for intermediate results. A typical query construction could
715
- look as follows:
716
-
717
- var qInterestingPosts = Post.$query().$where(/* condition */)
718
- .$where(/* further condition */)
719
- .$project(/* projection paths */)
720
- .$limit(5);
721
-
722
- The final query is executed by invoking its `$execute()` method, and is
723
- the only asynchronous operation in unmanaged mode:
724
-
725
- qInterestingPosts.$execute(tx, function (error, result) { … });
726
-
727
- It is important to note again that the result of the query is a plain
728
- value, not an entity instance managed by node-cds.
729
-
730
-
731
- ### Projections
732
-
733
- The `$project()` method specifies the fields the query should return:
734
-
735
- var qTitleAndComments = Post.$query().$project({
736
- Title: true,
737
- Comments: {
738
- Author: {
739
- Name: "CommentAuthorName"
740
- },
741
- Text: {
742
- Text: true
743
- }
744
- }
745
- });
746
- qTitleAndComments.$execute(tx, function (error, result) { … });
747
-
748
- The list of projected fields is a JavaScript object, where desired
749
- fields are marked by either true or a String literal such as
750
- `CommentAuthorName` denoting an alias name. Above query may thus
751
- yield the result:
752
-
753
- {
754
- Title: "First Post!",
755
- Comments: {
756
- Author: {
757
- CommentAuthorName: "Bob"
758
- },
759
- Text: {
760
- Text: "Can we prevent this by software?"
761
- }
762
- }
763
- }
764
-
765
- Note that the actual database query automatically `JOIN`s all required
766
- tables based on the associations involved. For above example, the
767
- generated SQL looks like (omitting the package prefix from the table
768
- name for readability):
769
-
770
- SELECT "t0"."Title" AS "t0.Title",
771
- "t0.Comments.Author"."Name" AS "t0.Comments.Author.Name",
772
- "t0.Comments"."Text.Text" AS "t0.Comments.Text,Text"
773
- FROM "bboard.post" "t0"
774
- LEFT OUTER JOIN "bboard.comment" "t0.Comments" ON "t0"."pid"="t0.Comments"."Post.pid"
775
- LEFT OUTER JOIN "bboard.user" "t0.Comments.Author" ON
776
- "t0.Comments"."Author.uid"="t0.Comments.Author"."uid"
777
- LEFT OUTER JOIN "bboard.user" "t0.Author" ON "t0"."Author.uid"="t0.Author"."uid"
778
-
779
-
780
- ### Selections using $where
781
-
782
- The `$where()` method filters the query result by some conditional
783
- expression. For example, to select all posts which were commented by
784
- a person with the same name as the post author, we write:
785
-
786
- var qStrangePosts = qTitleAndComments.$where(
787
- Post.Comments.Author.Name.$eq(Post.Author.Name));
788
-
789
- References to fields and associations such as `Comments` are available
790
- as properties of the entity object, e.g., `Post.Comments`. As in the
791
- case with projections, node-cds generates all required `JOIN`s for
792
- associations referenced by the conditions automatically, even if they
793
- are not part of the current projection.
794
-
795
- To build complex query expressions, the node-cds expression language
796
- provides a number of predefined operators that work on all data types:
797
-
798
- * `$eq`, `$ne` for SQL equality and inequality, resp.
799
- * `$gt`, `$lt`, `$gt`, `$le` for the SQL operators `>`, `<`, `<=`, `>=`, resp.
800
- * `$null` for the SQL operator `IS NULL`
801
- * `$like`, $unlike for the SQL operators `LIKE` and `NOT LIKE`
802
- * `$and`, $or for SQL junctors `AND` and `OR`
803
-
804
- A more complex selection statement could thus look like
805
-
806
- Post.$query().$where(
807
- Post.Tags.Name.$eq("+1").$and(Post.Rating.$lt(2).$or(Post.Rating.$gt(5))))
808
-
809
- yielding the following SQL query:
810
-
811
- SELECT "t0"."Created" AS "t0.Created",
812
- "t0"."Rating" AS "t0.Rating",
813
- "t0"."Parent.pid" AS "t0.Parent,pid",
814
- "t0"."Author.uid" AS "t0.Author,uid",
815
- "t0"."Text.Lang" AS "t0.Text,Lang",
816
- "t0"."Text.Text" AS "t0.Text,Text",
817
- "t0"."Title" AS "t0.Title",
818
- "t0"."pid" AS "t0.pid"
819
- FROM "bboard.post" "t0"
820
- LEFT OUTER JOIN "bboard.tags_by_post" "t0.Tags$viaEntity"
821
- ON "t0.Tags$viaEntity"."Post.pid" = "t0"."pid"
822
- LEFT OUTER JOIN "bboard.tag" "t0.Tags" ON "t0.Tags$viaEntity"."Tag.tid" = "t0.Tags"."tid"
823
- WHERE ("t0.Tags"."Name" = '+1') AND (("t0"."Rating" < 2) OR ("t0"."Rating" > 5))
824
-
825
- For other SQL operators not part of the node-cds expression language
826
- you may use generic operators such as $prefixOp
827
-
828
- qStrangePosts = qStrangePosts.$where(Post.Rating.$prefixOp("SQRT").$gt(1));
829
-
830
-
831
- ### Selections using $matching
832
-
833
- The `$matching()` method provides an alternative way to specify
834
- conditional expressions using the JSON-like syntax of the `$find()`
835
- method (see above).
836
-
837
- var q1 = Posts.$query().$matching(
838
- { Rating: { $gt: 2 });
839
- var q2 = Posts.$query().$matching(
840
- { Rating: { $ge: 1, $lt: 5 }, Parent: { $null: false } });
841
-
842
- The main difference between $matching() and $findAll() is that the
843
- former returns an unmanaged, plain value and ignores all unpersisted
844
- changes to any entity instances.
845
-
846
- We can think of the JSON-like conditional expression as a “template”
847
- that the result should match. Compared to the node-cds expression
848
- language used by `$where()`, the matching syntax is more concise, but
849
- also less expressive. Also note that the expression language does not
850
- apply to managed queries, e.g., to `$find()`.
851
-
852
-
853
- ### Calculated Fields and Aggregations
854
-
855
- Arbitrary calculated values may be added to the result set by using
856
- the $addFields() method. As an example, we return the square root of
857
- the post’s rating as an additional field `MyRating`:
858
-
859
- var qRootedRatings = Posts.$query().$addFields(
860
- { MyRating: Post.Rating.$prefixOp("SQRT") });
861
-
862
- Aggregations are a special case of calculated fields that combine the
863
- $addFields operator with an additional $aggregate() method. For
864
- example, to retrieve the average rating per user, we would write:
865
-
866
- var qUserRating = Post.$query()
867
- .$aggregate({ Author: { uid: true, Name: true } })
868
- .$addFields({ AverageRating: Post.Rating.$avg() });
869
-
870
- In SQL terms, the `$aggregate` operator creates a `GROUP BY` expression
871
- for the specified paths and automatically projects the result on
872
- those. For an even more restrictive projection you may replace the
873
- true by a false in the `$aggregate` call:
874
-
875
- var qUserRating = Post.$query()
876
- .$aggregate({ Author: { uid: false, Name: true } })
877
- .$addFields({ AverageRating: Post.Rating.$avg() });
878
-
879
- This will remove the users’ IDs from the in the result set.
880
-
881
- Currently, node-cds supports aggregation operators `$avg`, `$sum`, `$count`,
882
- `$max`, and `$min`.
883
-
884
-
885
- ### Ordering, Size Limits, and Duplicate Elimination
886
-
887
- The order of the result set is defined by the `$order()` method. Each
888
- order criteria contains a property by with an expression according
889
- which to order. Optionally each criterion can contain a flag desc to
890
- require a descending order and a nullsLast flag. The following
891
- example uses two criteria to first order descending by rating and then
892
- order ascending by author name:
893
-
894
- var qOrderedPosts = Post.$query()
895
- .$order({ $by: Post.Rating, $desc: true },
896
- { $by: Post.Author.Name });
897
-
898
- The $distinct operator removes duplicates from the result set. To get
899
- the set of all ratings we can write:
900
-
901
- var qAllRatings = Post.$query().$project({ Rating: true }).$distinct();
902
-
903
- The number of records returned may be specified by using the $limit
904
- operator, which introduces the `LIMIT` and `OFFSET` keywords into the SQL
905
- query:
906
-
907
- // skip posts 1-3, return posts 4-8
908
- var qNextFivePosts = qStrangePosts.$limit(5, 3);
909
-
910
-
911
- Entity Manegement and Consistency
912
- ---------------------------------
913
-
914
- Entities retrieved from the database are stored in the entity manager
915
- cache. Any subsequent query for that entity will be served from the
916
- cache instead of the database.
917
-
918
- It is important to realize that if we modify an entity instance in
919
- memory, then all node-cds queries for that entity instance through
920
- `$get()` and `$find()` will return the modified, in-memory version of the
921
- entity, even if it hasn’t been persisted to the database yet.
922
-
923
- // assume post #1 and post #2 share same author alice@sap.com
924
- var post1 = tx.$get(Post, { pid: 1 });
925
- post1.Author.Email = "alice@saphana.com";
926
- var post2 = tx.$get(Post, { pid: 2 });
927
-
928
- In above example, the value of `post2.Author.Email` equals the new value
929
- `alice@saphana.com`, even though post1 has not been `$save()`ed yet.
930
-
931
- An unmanaged query, on the other hand, will ignore unpersisted changes
932
- to our data and return the database view instead, so continuing the
933
- example from above,
934
-
935
- var post2_value = Post.$query().$matching({ pid: 2 }).$execute();
936
-
937
- will yield `post2_value.Author.Email === "alice@hana.com"`.
938
-
939
- You may use a transaction rollback to revert to the last committed
940
- state of your data, irrespective of whether data was persisted or not
941
- (see below).
942
-
943
-
944
- ### Associations
945
-
946
- There are some additional subtleties about the consistency of
947
- CDS-based associations that impose certain restrictions on using
948
- backlinks and entity links in managed mode.
949
-
950
- For backlink associations, the associated instances are stored in an
951
- array in the parent instance:
952
-
953
- tx.$get(Post, { pid: 1 }, function (error, post) {
954
- post.Comments.forEach(function (comment) { … });
955
- });
956
-
957
- For any given target instance `t` with backlink `b` there are two ways to
958
- express that `t` is a target of backlink association `a` of parent `p`:
959
-
960
- 1. `t` is a member of `p.a`
961
- 2. `t.b` equals `p`
962
-
963
- node-cds uses (2) exclusively to express the relationship between p
964
- and t. Supporting association updates through the association array,
965
- i.e., option (1), would impose an unduly amount of runtime overhead
966
- on the application and may lead to conflicting membership states.
967
-
968
- For convenience, however, the `$save` function will set the backlink
969
- of newly created targets currently stored in `p.a` to `p`. Thus,
970
- `p.a` may be used to add newly created instances, but it cannot be
971
- used to add or remove already existing instances.
972
-
973
- As a consequence of the above, updating an instance with backlinks
974
- may lead to situations where `p.a` contains elements that are not part of
975
- the association or where `p.a` misses elements that are part of the
976
- association. To re-sync the association array `p.a` all backlink
977
- associations have a `$reload()` function that will update the
978
- current array:
979
-
980
- tx.$get(Post, { pid: 1 }, function (error, post) {
981
- var firstComment = post.Comments[0];
982
- firstComment.Parent = 0; // remove first comment
983
- post.Comments.$reload(function (error, comments) {
984
- // comments == post.Comments
985
- // firstComment not in comments
986
- });
987
- });
988
-
989
- Saving an instance with backlinks will update all array members
990
- individually, but this will not change their backlinks. Thus, simply
991
- adding and/or removing members from the association array and
992
- `$save()`ing the parent will not change the relationship between parent
993
- and target instances.
994
-
995
- tx.$get(Post, { pid: 1 }, function (error, post) {
996
- post.Comments.splice(1); // incorrect: won’t remove comment
997
- tx.$save(post, function (error, updatedPost) {
998
- // $reload restores first comment back to post.Comments
999
- });
1000
- });
1001
-
1002
- When saving an instance with backlink associations, the `$reload()`
1003
- function will be invoked automatically.
1004
-
1005
- For many-to-many associations, on the other hand, the association
1006
- array is the primary means to control the membership of individual
1007
- target instances:
1008
-
1009
- tx.$find(Tag, { Text: "cool" }, function (error, tag) { // find certain tag
1010
- tx.$get(Post, { pid: 1 }, function (error, post) { // get particular post
1011
- post.Tags.push(tag); // attach tag to post
1012
- tx.$save(post, callback); // update database
1013
- });
1014
- });
1015
-
1016
- This example attaches the tag “cool” to the first post without
1017
- modifying any already existing tags.
1018
-
1019
- Note that the direct manipulation of linking entities is discouraged,
1020
- as this may lead to inconsistencies between the views on the
1021
- association arrays of the entities with many-to-many associations and
1022
- their link tables:
1023
-
1024
- post.Tags = [ tag1 ]; // post has one tag
1025
- tx.$save({ $entity: TagsByPost,
1026
- lid: 69, Tag: tag2, Post: post }, callback); // don’t do this!
1027
-
1028
- Unmanaged associations are handled very similar to backlink
1029
- associations in that the target association status cannot be
1030
- controlled by array membership:
1031
-
1032
- tx.$get(PostWithTopReplies, { pid: 101 }, function (error, thread) { … });
1033
-
1034
- But unlike managed associations such as those defined by
1035
- backlinks, an unmanaged association is static and ignores both updates
1036
- to the database and to the cache unless reloaded explicitly.
1037
-
1038
- var reply = thread[0];
1039
- reply.Rating = 0;
1040
- tx.$save(reply);
1041
-
1042
- // reply still contained in thread.TopReplies even
1043
- // though Rating >= 3 no longer holds
1044
- var i1 = thread.TopReplies.indexOf(reply); // === 0
1045
-
1046
- // explicit reload from database
1047
- thread.TopReplies.$reload(function (error, instance) {
1048
- // now reply is no longer contained in thread.TopReplies
1049
- var i2 = thread.TopReplies.indexOf(reply); // === -1
1050
- });
1051
-
1052
- Saving the parent of an unmanaged association will also save all
1053
- elements stored in the association array. Finally, before invoking
1054
- the callback function, the `$reload` function of the parent is
1055
- called.
1056
-
1057
- Discarding an instance will delete the root instance, but not
1058
- associated entities by default, even for one-to-one associations. If
1059
- you do want to delete associated entities as well, you can add a
1060
- $cascadeDiscard property to your entity import statement:
1061
-
1062
- cds.$importEntity({
1063
- $entity: "…",
1064
- $fields: {
1065
- MyAssoc: {
1066
- $association: {
1067
-
1068
- $cascadeDiscard: true
1069
- }
1070
- }
1071
- }
1072
- }, callback);
1073
-
1074
- node-cds supports explicit cascading for deletion only at the moment.
1075
- All other operations such as creation, tracking, and updates are
1076
- always cascaded to all associated entities automatically.
1077
-
1078
- Note that node-cds currently doesn’t support orphan removal. It is
1079
- left to the application to maintain integrity of associations and
1080
- references. You can let HANA help you there by defining key
1081
- constraints for your associations.
1082
-
1083
-
1084
- Transactions
1085
- ------------
1086
-
1087
- All node-cds instance operations are tied to transactions. To open a
1088
- new transaction, call the corresponding node-cds function:
1089
-
1090
- cds.$getTransaction(function (error, tx) { … });
1091
-
1092
- By default, all `$save()` and `$discard()` operations will auto
1093
- commit upon completion. To change this behavior you can set auto
1094
- committing explicitly:
1095
-
1096
- tx.$setAutoCommit(<boolean>);
1097
-
1098
- When not in auto commit mode, transactions can be explicitly committed
1099
- or rolled back:
1100
-
1101
- tx.$commit(callback);
1102
- tx.$rollback(callback);
1103
-
1104
- If you call `$rollback`, the instance cache for that transaction will be
1105
- reset. You need to make sure that you do not use any references pointing to
1106
- obsolete instances predating the cache reset.
1107
-
1108
- Note that auto commit refers to node-cds operations, not database operations.
1109
- In other words, if you `$save` an entity with a single target entity the two
1110
- low-level `INSERT` statements are grouped into a single node-cds transaction
1111
- that will `commit` or `rollback` together.
1112
-
1113
- When done, close and release the transaction by calling its `$close`
1114
- method:
1115
-
1116
- tx.$close();
1117
-
1118
- If your application is passed an existing database connection you can
1119
- pass the connection to $getTransaction():
1120
-
1121
- cds.$getTransaction(<dbconn>, function (error, tx) { … });
1122
-
1123
- This will reuse the existing connection instead of requesting a new
1124
- connection from the pool. If `dbconn` is in state disconnected node-cds
1125
- will try to open the connection. Note, however, that multiple calls
1126
- using the same connection will also return the same transaction!
1127
-
1128
- Calling `$close()` on a transaction tied to an existing database
1129
- connection will not have any effects.
1130
-
1131
-
1132
- Support for geospatial HANA data types
1133
- --------------------------------------
1134
-
1135
- node-cds also provides handling of entities with spatial attributes:
1136
-
1137
- entity attraction {
1138
- ...
1139
- position: hana.ST_GEOMETRY(4326);
1140
- };
1141
-
1142
- For processing such entities node-cds uses an extension 'cds-queries-geo',
1143
- which can be optinally be required and allows to seamlessly create/insert
1144
- spatial objects and perform spatial functions with/on them in combination
1145
- with normal node-cds query API. For example a query like
1146
-
1147
- attractions.$query().$where( attractions.position.$distance( geo.stPoint([49.008981, 8.403897])).$le(1500))
1148
-
1149
- would result in:
1150
-
1151
- [
1152
- { "id":5 ,
1153
- "name":"SAP Karlsruhe" ,
1154
- "info":"SAP location Karlsruhe." ,
1155
- "year":1999 ,
1156
- "position": {"type":"Point","coordinates":[49.013143,8.424231]}
1157
- },
1158
- ...
1159
- ]
1160
-
1161
- The extension allows for converting the resulting spatial objects to
1162
- [GeoJSON](http://geojson.org/) objects so that results can seamlessly
1163
- be processed in JavaScript.
1164
-
1165
-
1166
- XSJS Compatibility Mode
1167
- -----------------------
1168
-
1169
- For code in the xsjs compatibility mode, an extension
1170
- compatible with the XS Basic library XSDS can be used.
1171
-
1172
- Note, that it is intended to be used *only within the XSJS
1173
- Compatibility Layer*.
1174
-
1175
- To initialize it from an xsjs file, it needs to be invoked as follows:
1176
-
1177
- var conn = $.db.getConnection();
1178
- var cds = $.require("cds").xsjs(conn);
1179
-
1180
- Note, that the connection to the database is managed by the connection
1181
- provided in the argument to the xsjs call. The connection object can
1182
- be modified through the `cds.Transaction` object. E.g. you can close
1183
- the connection by `cds.Transaction.$close();`.
1184
-
1185
- Like in plain node-cds, entities can then be imported following the
1186
- syntax and the synchronous style of XSDS:
1187
-
1188
- var Post = cds.$importEntity("sap.hana.xs2.cds.examples.data", "bboard.post");
1189
-
1190
- Note, that namespace and entity name are submitted as separate
1191
- arguments. Moreover only one entity at a time is imported. In order to
1192
- enhance the entity, a third argument can be passed, as demonstrated in
1193
- the following example:
1194
-
1195
- var Post = cds.$importEntity("sap.hana.xs2.cds.examples.data", "bboard.post",
1196
- { Comments:
1197
- { $association:
1198
- { $entity:
1199
- "sap.hana.xs2.cds.examples.data::bboard.user",
1200
- $viaBacklink: "Post"
1201
- }
1202
- }
1203
- });
1204
-
1205
- Next, entities can be retrieved in managed or unmanaged mode, using
1206
- the operations already known from plain node-cds, but in the
1207
- synchronous approach of XSJS.
1208
-
1209
- For example, the $get method of an entity will retrieve an entity
1210
- instance by its key:
1211
-
1212
- var post = Post.$get({ pid: 101 });
1213
-
1214
- Creating a new instance is just as simple as invoking the 'new'
1215
- operator and persisting the resulting object:
1216
-
1217
- // create new post
1218
- var newpost = new Post({ pid: 102,
1219
- Title: "New Post",
1220
- Text: { Text: "Hello BBoard!", Text: "EN" },
1221
- Author: post.Author,
1222
- Rating: 1,
1223
- Created: new Date() });
1224
- newpost.$save();
1225
-
1226
- For complex ad-hoc queries the same query builder as in plain node-cds
1227
- can be used. The $query method of the entity constructor returns a
1228
- query object for the step-wise building of complex queries:
1229
-
1230
- var results = Post.$query().$project({
1231
- Author: {
1232
- Name: true,
1233
- Email: true
1234
- },
1235
- Title: true
1236
- }).$execute();