In our previous post we introduced XS Data Services (XSDS), a native client for Core Data Services and query builder for XS JavaScript.  In this blog post we will show how we can use XSDS to work with CDS entity instances as plain JavaScript objects.

Working with Managed CDS Entity Instances

Once our entity imports are in place we can use the resulting constructor functions to work with our entities in our application. For this, XSDS provides two different modes to interact with the database: managed mode and unmanaged mode.  Managed mode works with entity instances, i.e., singleton objects with “personality” which correspond one-to-one to database records. Unmanaged mode, on the other hand, works with plain values that are simple nested JSON-like structures.

In this post we’ll be looking at managed mode.  To retrieve an existing entity instance in managed mode, we may query it by its key:

var post = Post.$get({ pid: 14 });

This will retrieve the post with pid = 14 if it exists, or null otherwise.  The $get() method requires that all key properties be supplied.

The $find() and $findAll() methods are more general in that they return one entity or all entities, respectively, which satisfy a given set of property values:

var users = User.$findAll({ Name: “Bob” });  // return all users named Bob

The instance filter expression may be built using the following basic expression language that is valid for all JavaScript and SQL types:

<expr>  ::=  { <cond>, <cond>, … }

<cond>  ::= prop: value | prop: { <op>: value, … }

<op>    ::= $eq | $ne | $lt | $le | $gt | $ge | $like | $unlike | $null

All conditions in an expression are joined by logical-AND. The expression { prop: value } is a shorthand for { prop: { $eq: value } }.

The comparison operators $eq, $ne, $lt, $le, $gt, $ge apply to all suitable types if the value is supplied in appropriate format:

$findAll({ p: { $lt: 100 } });                // returns all instances where p < 100

$findAll({ p: { $ge: 100, $le: 200 } });      // … p between 100 and 200

$findAll({ p: “Bob”, q: { $ne: “Jones” } });  // … p is Bob but q is not Jones

The $like and $unlike operators can be used for SQL-like pattern matching:

User.$findAll({ Name: { $like: “B%” } });     // returns all users whose name starts with B

User.$findAll({ Name: { $unlike: “J..” } });  // returns Bill but not Jim

The $null operator checks if a given property is NULL in the database:

Post.$findAll({ Rating: { $null: true } });    // returns posts with unknown rating

Post.$findAll({ Created: { $null: false } });  // returns posts with known authors

Note again that in the second example, $null checks for NULL, not the $none value of the Author association in Post!  In JavaScript terms this is the difference between Post.Author === undefined and Post.Author === null.

Expressions are evaluated by the database but also by JavaScript when checking the entity cache for matching instances (see section on Entity Management below).  This can yield unexpected results due to JavaScript’s peculiar semantics, especially for dates:

$findAll({ myDate: { $eq: new Date(2014, 0, 1) } }); // always fails (in JavaScript)

$findAll({ myDate: { $lt: new Date(2014, 0, 1) } }); // OK

Managed queries are inherently limited in their expressiveness, as the XSDS runtime needs to check its instance cache against the filter condition provided to $find. Applications requiring advanced query capabilities should use unmanaged queries described in the next section. Of course, both managed and unmanaged queries can be used side by side.

Updating Entities

Entity instances are regular JavaScript objects and can be used and modified as one would expect:

post.Ranking++;

post.Author = users[0];

post.Author.Name = “Newt”;

All changes are made in memory only.  If we want to persist our changes for an instance we invoke its $save() method:

post.$save();   // update post and its associated entity instances

Calling $save() will flush our in-memory changes of that instance to the database, following all associations reachable from the root instance.  Only entities actually changed will be written to the database.

Key properties must not be changed; any attempts to persist an entity with changed key values will raise a runtime exception:

var post = Post.$get({ pid: 14 });

post.pid++;         // bad idea!

post.$save();       // throws runtime exception

There is an additional batch persist for persisting multiple instances in one operation:

Post.$saveAll([ post1, post2 ]);   // persist post1 and post2

Note that modified instances in memory always take precedence over their unmodified versions on the database.

Creating new Entities

The entity constructor is used to create new entity instances:

var user = User.$find({ Name: “Alice” });

var post = new Post({ pid: 101, Title: “New Post”, Text: “Hello BBoard!”,

                     Author: user, Rating: 1, Created: new Date() });

post.$save();

Key properties for which an HDB sequence has been supplied may be omitted from the constructor call.

New entities will be put under entity management right away, i.e., the $find() and $findAll() methods will return them if their properties match.  They will not be written to the database, however, until explicitly persisted with the $save() method.

For entities with associations there is an alternative method to create new instances:

var post = Post.$build({ pid: 102, Title: “Another Post”,

                         Text: “New post, same author”,

                        Author: { uid: 1, Name: “Alice” } });

var post = Post.$build({ pid: 103, Title: “Another Post”,

                         Text: “New post, new author”,

                        Author: { uid: 2, Name: “Newt” } });

The $build method will automatically create or retrieve associated target instances matching the properties supplied, whereas new requires that all targets be supplied as instances:

var post = new Post({ pid: 104, Title: “Fail Post”,

                      Text: “This will not work”,

                      Author: { uid: 2, Name: “Newt” } });

post.$save();  // fails, post.Author is not an entity instance

By passing true as optional second argument to $build XSDS will not try to retrieve already existing instances.

Discarding Entities

Retrieved entities are stored in the entity manager cache and subject to general JavaScript garbage collection.  To permanently delete an entity instance from the database the $discard() method is used:

post = Post.$get({ pid: 99 });

post.$discard();

Note that after calling $discard() on an instance, the actual JavaScript object remains in memory as an unmanaged entity instance, i.e., $find() will no longer not return references to it.  It is up to the application to not use any remaining references to the object that may still be stored in some JavaScript variables.

The $discardAll() method for batch discards works analogous to the $saveAll() method:

Post.$discardAll([ post1, post2 ]);   // discards post1 and post2

For the special use case of deleting instances on the database without instantiating them in memory first XSDS provides the $delete() operation for unmanaged deletes:

Post.$delete(<cond>);  // deletes all posts matching <cond>, BYPASSING CACHE!

An unmanaged delete will delete all matching records on the database, ignoring the state of the entity cache.  Thus, the set of affected entity instances may differ from that of the sequence

var posts = Post.$findAll(<cond>);

Post.$discardAll(posts);

Mixing managed and unmanaged data operations is dangerous, so please use $delete() with care.

Lazy Navigation

By default, all associations are eagerly resolved, i.e., association properties store a reference to their associated entity instance. For heavily connected data this may lead to very large data structures in memory.

To control which associations are being followed the association may be declared $lazy:

var LazyPost = XSDS.$importEntity(“sap.hana.democontent.xsds”, “bboard.post”, {

    Parent: { $association: { $lazy: true } }

});

A lazy association will delay the retrieval of the associated entity or entities until the property is actually accessed:

var post = LazyPost.$get({ pid: 102 });  // retrieves single Post and Author from database

post.Rating++;

if (post.Author.Name === “Lassie”) {

    var parent = post.Parent;   // retrieve parent from database, if it exists

    if (parent)

        parent.Rating++;

}

post.$save();   // persist any changes made

The first time the lazy association is accessed the associated entity is queried from the entity cache or the database.  Once a lazy association has been resolved it is a normal property of its parent entity instance.

Lazy associations may be chained and updated transparently:

var post = LazyPost.$get({ pid: 103 });  // retrieve single post

post.Parent = new LazyPost( … );         // sets new parent post, old parent is never loaded

post.$save();

Note that updates to an entity instance will not update associated lazy instances if they haven’t been followed yet!

A lot may happen between the retrieval of some entity instance and the navigation to any of its lazy association targets.  It is left to the application to ensure the consistency of data.

Entity Management and Consistency

Entities retrieved from the database are stored in the entity manager cache.  Any subsequent query for that entity will be served from the cache instead of the database.

It is important to realize that if we modify an entity instance in memory, then all XSDS queries for that entity instance through $get(), $find(), and $findAll() will return the modified, in-memory version of the entity, even if it hasn’t been persisted to the database yet.

// assume post #1 and post #2 share same author alice@sap.com

var post1 = Post.$get({ pid: 1 });

post1.Author.Email = alice@saphana.com;

var post2 = Post.$get({ pid: 2 });

In above example, the value of post2.Author.Email equals the new value “alice@saphana.com”, even though post1 has not been $save()ed yet.

An unmanaged query, on the other hand, will ignore unpersisted changes to our data and return the database view instead, so continuing the example from above,

var post2_value = Post.$query().$matching({ pid: 2 }).$execute();

will yield post2_value.Author.Email === “alice@hana.com.

You may use a transaction rollback to revert to the last committed state of your data, irrespective of whether data was persisted or not (see below).

Associations

There are some additional subtleties about consistency when working with CDS-based associations that impose certain restrictions on using backlinks and entity links in managed mode.

For backlink associations, the associated instances are included in the parent entity instance, be it eagerly or lazily:

var post = Post.$get({ pid: 69 });

var count = post.Comments.length;   // post.Comments contains all associated Comment instances

In JavaScript parlance the type of post.Comments is called “array-like”: while the object is not an instanceof Array, it supports all non-mutating Array.prototype functions and may be passed to functions that expect an array:

post.Comments.forEach(function(comment) { … });  // array-like

var comments = post.Comments.slice();            // convert to real but detached array

Another important property of post.Comments is that the array is read-only, so you cannot reassign individual array elements or mutate the array:

post.Comments.pop();                    // error: read-only

post.Comments = [];                     // error: read-only

post.Comments.length = 0;               // error: read-only

post.Comments[0] = new Comment(…);      // error: read-only

post.Comments[0].Author.Name = “Newt”;  // OK: array elements are mutable

Consequently, the association relation has to be updated through the members’ backlinks.  Whenever a backlink changes, the corresponding association arrays are updated automatically:

var c0 = post.Comments[0];

post.Comments.splice(0, 1);             // error: array is read-only

post.Comments[0].Parent = null;         // correct: change backlink

var index = post.Comments.indexOf(c0);  // index == -1 -> c0 immediately removed from array

Supporting association updates through the association array would impose an unduly amount of runtime overhead on the application, which is why backlink association arrays are read-only.

For many-to-many associations, on the other hand, the association array is a native JavaScript array that may be manipulated in any way that JavaScript supports:

vartag = Tag.$find({ Text: “cool” });  // find certain tag

var post = Post.$get({ pid: 69 });      // get particular post

post.Tags.push(tag);                    // attach tag to post

post.$save();                           // update database

This example attaches the tag “cool” to the post with pid === 69 without modifying any already existing tags.

Note that the direct manipulation of linking entities is discouraged, as this may lead to inconsistencies between the views on the association arrays of the entities with many-to-many associations and their link tables:

post.Tags = [ tag1 ];          // post has one tag

var link = new TagsByPost({ lid: 69, Tag: tag2, Post: post }); // don’t do this!

link.$save();                  // post now has two tags on database!

var count = post.Tags.length;  // but count === 1 not updated!

For unmanaged associations, the resulting property is again a read-only array-like object:

var thread = PostWithTopReplies.$get({ pid: 101 });

for (var i = 0; i < thread.TopReplies.length; ++i)

authorCredits[thread.TopReplies[i].Author.uid]++

But unlike managed associations such as those defined by backlinks, an unmanaged association is static and ignores the entity cache containing unsaved updates.  To re-sync the association array with the current database state the $reread method must be called explicitly:

var reply = thread[0];

reply.Rating = 0;

reply.$save();

// reply still contained in thread.TopReplies even though Rating >= 3 no longer holds

var i1 = thread.TopReplies.indexOf(reply);  // === 0

thread.TopReplies.$reread();

// now reply is no longer contained in thread.TopReplies

var i2 = thread.TopReplies.indexOf(reply);  // === -1

Note, however, that invoking $reread will still not reflect unsaved changes contained in the entity cache only!  In other words, if we removed reply.$save() from our example above, variable i2 would still contain a value of 0.  If your application relies on a fully consistent view on all data at all times unmanaged associations should be used cautiously.

Discarding an instance will delete the root instance, but not associated entities by default, even for one-to-one associations.  If you do want to delete associated entities as well, you can add a $cascadeDiscard property to your association definition:

var PostCD = XSDS.$importEntity(“…”, “…”, {

    Comments: {

        $association: {

            $entity: “Comment”,

            $viaBacklink: “Parent”,

            $cascadeDiscard: true

        }

   }

});

XSDS supports explicit cascading for deletion only at the moment. All other operations such as creation, tracking, and updates are always cascaded to all associated entities automatically.

Note that XSDS currently doesn’t support orphan removal. It is left to the application to maintain integrity of associations and references.  You can let HANA help you there by defining key constraints for your associations.

Converting between managed and unmanaged values

XSDS offers applications both managed and unmanaged views of their data.  In general, these views should not be mixed to avoid confusion and potential data corruption, but for some use cases a careful conversion of managed and unmanaged data is required.

To convert managed entity instances into unmanaged values, the $clone method yields a detached plain value without modifying the instance:

var value = instance.$clone();

$clone() creates an independent deep copy of instance where all references have been resolved into separate plain JavaScript objects.

To create managed instances from unmanaged values, the situation is slightly more complex.  In simple cases without associations, the new operator may be used:

var instance = new Entity(value);

Note, however, that the instance created by new will only be valid if value supplies actual instances for all associated targets of Entity.  If value is the result of an unmanaged query this is unlikely to be the case.

For the general case the $build method will take an arbitrary unmanaged value and construct a valid instance from that value by a combination of new and $get operations.  The result will be a valid instance, including associations, that reflects the data updates originating from the unmanaged value supplied:

var post = Post.$get({ pid: 101 });

–> post === {

     “pid”: 101,

     “Title”: “First Post!”,

     “Author”: { “uid”: 1, “Name”: “Alice”, “Email”: “alice@sap.corp”, … },

     “Rating”: 1,

     “Created”: “2014-08-14T12:22:50.000Z”  }

var updatedPost = Post.$build({ pid: 101, Rating: 3, Author: { uid: 1, Name: “Newt” } });

–> post === updatedPost === {

     “pid”: 101,

     “Title”: “First Post!”,

     “Author”: { “uid”: 1, “Name”: “Newt“, “Email”: “alice@sap.corp”, … },

     “Rating”: 3,

     “Created”: “2014-08-14T12:22:50.000Z”  }

If $build cannot get an existing instance for the key values supplied it will try to create a new instance using the new operator. When constructing all new instances you can greatly improve performance by passing true as second argument to $build.  XSDS will then skip getting already existing instances and invoke new for all instances right away.

Converting between managed and unmanaged values may be a costly operation, especially when using $build.  We thus recommend designing your application carefully in advance to minimize the number of conversions required.

Outlook

This concludes our introduction to XSDS managed mode for now. In a follow-up post we’ll turn to unmanaged mode and its powerful query interface.

To report this post you need to login first.

11 Comments

You must be Logged on to comment or reply to a post.

  1. Fabio Pagoti

    Hi Ralph Benzinger

    Is the example from section “Entity Management and Consistency” really correct?

    // assume post #1 and post #2 share same author alice@sap.com

    var post1 = Post.$get({ pid: 1 });

    post1.Author.Email = alice@saphana.com;

    var post2 = Post.$get({ pid: 2 });

    If you changed post1… why would post2 return alice@saphana.com if it’s a different database record? It seems you wanted to used { pid: 1} on both $get calls. Could you please confirm that?

    Thanks!

    (0) 
    1. Ralph Benzinger Post author

      Hello Fabio,

      Yes, the example is correct, but maybe not clear enough.

      Posts and Users are stored in different database tables, and each Post record contains a reference Author to some User record.  In the example, Post records pid=1 and pid=2 both point to the same User record of Alice.  The primary key of that record isn’t shown in the example, as the link between Posts and Users is handled automatically by XSDS.

      The point of the example is exactly to show that since both Posts point to the same User, modifications to the User should be visible for all Posts (within the transaction), even if the change hasn’t been persisted to the database yet.

      Hope that makes sense.

      Regards,

      Ralph

      (0) 
        1. Ralph Benzinger Post author

          Well, it’s complicated … or maybe not, but difficult to state precisely.

          In JavaScript, objects are values, and the instances you retrieve are JavaScript objects, so they’re also values.

          What we’d like to differentiate is managed instances and unmanaged values.  If you have an entity E with just an “id: INT” and a “val: INT” column, then { id: 1, val: 2 } would be a valid value for that entity.  In fact, this is what you would get when using $query.

          The $get, on the other hand, gives you a managed instance that has the same properties but also some methods such as $save.  Those functions and internal housekeeping properties won’t show up in toString().

          But even more importantly, instances have an “identity” — at every point in time there’s only one existing JavaScript object for each instance.  Suppose you add an extra association “a: association to E” in E above, then you could define

          var inst = new E({ id: 2, val: 69 });  inst.a = inst;  inst.$save();

          The resulting instance is circular and cannot be expressed as an unmanaged value, because unmanaged values replicate re-occurring instances.

          So its all about objects and how they link to each other, not about their JSON representation or anything like that.

          Ralph

          (0) 
          1. Fabio Pagoti

            Right!

            Something I noticed recently is that you can only use $get once for the same object. So if you need the same instance over and over again it’s better to save it in a local reference for later use.

            But in any case I opened 4 or 5 threads here at SCN recently about XSDS… it’s been awesome to use it and learn apart from some flaws.

            The main problem I had with XSDS so far is that the $delete(condition) method really deletes all your records without even care if you pass a condition using a wrong column name. I see the danger in unmanaged stuff now.

            Well.. lesson learned, I’m only using $discardAll(array) now.

            Thanks!

            (0) 
            1. Ralph Benzinger Post author

              Hmm, that’s odd — you should be able to $get the very same instance over and over again, and always get the same JavaScript object for it.  In fact, that’s the whole point of $get.

              Do you have an example where this doesn’t work for you?

              (Your right with your comment about $importEntity in some other thread, though: that should be used only once.)

              Ralph

              (0) 
              1. Luis Lópiz Morales

                Hello Ralph,

                i’ve been experimenting problems using the $get method…

                if i call it twice in the same program:

                var user = userEntity.$get({ id: 1});

                var user2 = userEntity.$get.$get({ id: 1});



                it throws an error:

                Error: XSDS: entity manager: duplicate key value: #I1.0 (package.data::model.user({ id: 1, })

                I know the code looks stupid, but take it just as what it is, an example.

                Do i have to do anything with the first instance in order to call the second $get?

                I’ve tried saving the first instance, but still doesn’t work.

                Any help please? i could have just a global instance of the user, i know, but just for curiosity, i would like to know what’s the problem.

                thank you very much.

                (0) 
                1. Ralph Benzinger Post author

                  Hello Luis,

                  You should definitely be able to call $get as often as you like.  A “duplicate key” error may indicate an internal error, or more likely an erroneous or incomplete entity import.  It’s hard to make a guess here without context (I assume the $get.$get is just a typo).

                  Please try to isolate the problem as much as you can, and open a bug report if the problem persists and you think that your code is correct.

                  Regards,

                  Ralph

                  (0) 
                  1. Luis Lópiz Morales

                    Hello again,

                    this issue has appeared again, and i don’t know what to do about it…

                    if I call :

                    translationEntity.$get({id: 10});

                    twice, in the second call I get the mentioned error:

                    Error: XSDS: entity manager: duplicate key value: #I1.0 (package.data::model.translation({ id: 10, })

                    Here you can find the import I’m making. The sequence works fine at the time of insertion, i don’t know what can be happening…

                    var translationEntity = XSDS.$importEntity(pack, model + ".translation",
                    		{ id: { $key:   seqPath + 'translation"' }, 
                    		editable: { $init: 1 }, status: { $init: Status.creation } }	
                    );

                    Thank you so much for your help.

                    Best regards,

                    Luis

                    (0) 

Leave a Reply