Saturday, January 13, 2018

Oracle JET: Caching data

Problem Description: Oracle JET is a client side technology and mostly it uses REST services to fetch data from server. Some of data is used on multiple pages and it does not change very frequently so its a good approach to query such data once and then cache it. Next time when you need it refer from cache.


Solution:

In Oracle JET we have index.html, which serves as main (or base) page. All other pages (module) appears on it. Some modules might be static and some are attached dynamically using router. To some up at run time, things are like below image.


Based on URL different modules keep on changing but index.html (base page) is as it is. It implies view model associated with it (also called root viewmodel) is also available all the time.

Another thing that we should know is 'LifeCycle of ViewModel instances'. In general code for viewmodels are like below

define(['ojs/ojcore', 'knockout', 'jquery', 'appController'],
 function(oj, ko, $, app) {
    
    function PageXViewModel() {
      console.log("Instantiating PageXViewModel");
      var self = this;
 
    }

    return new PageXViewModel();
  }
);

If we see ViewModels are created using RequireJS define function. RequireJS is an AMD framework. It helps creating JS objects only when needed and once object is available, it associate it with a key. Object remains in memory always and you can access it with that key. All ViewModels are created like that. Which means ViewModel instace would get created when first time you access module associated with that ViewModel. Next time when you try to access ViewModel, you will not get a fresh instance, you will get same old instance instead.
In above example  PageXViewModel instance would get created only once, when we access PageX first time. After that if we go to PageY and come back to PageX, same instance of PageXViewModel will be reused. To confirm that you can see browser console log and find how many times "Instantiating PageXViewModel" is getting printed. You will find its only once, when PageX is loaded first time.

Effectively we can say if  page is showing a module (PageX), there are two instances available PageXViewModel and RootViewModel. We can use them to cache our data. 

With that in mind, in this blog we can see various strategy to cache our data.

There are different kinds of data that we show on pages
1. Transactional data: which keeps on changing and user expects it to be latest always. For example table showing employee data. We should not try to cache such data as user expectation is to see latest data always. As we know our ViewModel instances are getting reused so how should we make sure that every time, we go to a page data is re-fetched from server. ViewModel has a function called handleActivated. This is called by framework always when a view is loaded. We can use this function to fetch data from server. For example

 self.handleActivated = function(info) {

         var promise = $.ajax({
                 type: 'GET',
                 url: 'http://myservice-url',
                 contentType: "application/json; charset=utf-8",
                 crossDomain: true,
                 dataType: "json"
              })
 
  promise.then(function(data) {
            self.data = ko.observableArray(data);

          });
 
         return promise;

        
        
     };
In above code we are setting data property of ViewModel using a REST output and this code runs every time we load page, so data will always show new values from server whenever page is loaded.

2. Setup data: which rarely changes. For example data appearing in various drop-downs (lookup).  This kind of data is ideal for caching. In this example let say we have a lookup PRIORITY and it shows values as High (H), Medium (M), Low (L). We want to show it as drop-down on customer and profile pages.

  To cache that I have two step code
  a. Add below method in appcontroller.js

function ControllerViewModel() {
        var self = this;

        //initiate global cache with empty objects
        self.globals = {lookups: []};


        //core private method responsible for fetching lookup data using rest-service
        var fetchLookupData = function fetchLookupData(lookupType){
            
            var encodedString = 'Basic ' + btoa('username:password');
            var promise = $.ajax({
                 type: 'GET',
                 url: 'my-rest-url,
                 contentType: "application/json; charset=utf-8",
                 headers: {"Authorization": encodedString},
                 crossDomain: true,
                 dataType: "json"
              });

            promise.then(function(response) {
                  self.globals.lookups.push ( {"lookupType": lookupType, 
                                              "lookupValues": response.lookupValues});
                 

              }, function(error) {
                  //alertFactory.handleServiceErrors(error);
                  console.log(error); //Handle errors in a better way
              });

            return promise;
        }

        //method to put a logic to get lookup data from cache (global) first and if not present then from rest-service
        self.initLookpCache = function initLookpCache(lookupType){
          
            var lookups = self.globals.lookups.filter(function (lookup){
              return lookup.lookupType === lookupType;
            })
            if(lookups.length > 0){
              console.log("Lookup present in cache");
              return lookups[0];
              
            }
            else{
              console.log("Reading lookup from service");
              return fetchLookupData(lookupType);
            }
          
        }
      
         //Remaining standard code of appcontroller.js is not shown.

Now on page, which requires cached data we can add below lines. Add it in customer.js and profile.js both

self.handleActivated = function(info) {
        // Implement if needed
        console.log('running handleActivated from CustomerViewModel');
        
        var promise = app.initLookpCache('MY_LOOKUP_TYPE');
        Promise.resolve(promise).then(function(lookupTypeValues) {
            self.linkTypes = ko.observableArray(lookupTypeValues.lookupValues);
        });


        return promise;

      };


We have actually put a call to Root-ViewModel's initLookupCache
It can return either Promise or lookupvalue. Promise is returned if values are getting fetched from REST service call. LookupValues are returned directly if values are coming from cache.

Promise.resolve make sure that
        if input is promise object then wait for it to get completed and once completed then only run function specified in then.
        if input is not promise but direct values then directly runs the function and pass values as input.

If you run customer and profile pages, you will find below log
running handleActivated from CustomerViewModel
Reading lookup from service

Instantiating ProfileViewModel
running handleActivated from ProfileViewModel
Lookup present in cache

You can see lookup values are coming from cache second time. Good part is its incremental cache, as and when you need lookup data first time, it will get cached. You are not loading all lookup data upfront.

Few other tips

  a. If you want to cache a data which is required very frequently (almost on all pages) you can cache such data from appcontroller.js itself.


  c. If you want to cache a data, which is required only in very very few pages you can cache them in their respective page's ViewModel itself. For that instead of calling appcontroller's method you can have local method and populate ViewModel (self.data) variable. NOTE: Even these page's viewmodel are not getting destroyed so anything cached there is also present throughout. For local caching your code of handleActivated could be be something like
self.data = ko.observable();

self.handleActivated = function(info) {
   if(!self.data()){
      //Make reset service call and populate self.data
   }
}




Thats all.

No comments: