Pages

Tuesday, July 26, 2011

Using JS caching on serverside data with jquery datatables plugin

First of all, I would like to say that I have been working on lots of stuff but have had no time to update this blog. Recently I was speaking with my friend who checks my blogs regularly, and was inspired to make some time and post stuff that is useful for many. So, here is one of the things that I worked on and pretty useful.

Pagination is one of the most important concepts that must be implemented correctly for better performance of the database and that of your application. It is common sense to say that caching all the data on the server session is bad, or contacting database for every page is also bad; esp when working on tables with millions of records. The better alternative is to load and cache a good set of data on UI, and as and when needed update cache with more data. This removes caching done on server side, say in session, and negative performance impact that you get when you query database often while pagination.

Many use jquery framework on javascript side. One of the plugins that I have worked with is datatables. Located at http://www.datatables.net/ . There exist other frameworks like YUI for pagination and definitely other plugins for jquery framework for pagination. The datatables plugin works with server side data but does not provide good caching mechanism. Every new page is a request for getting data from the server to display that page. I wrote the below given javascript function that can be used with “fnServerData” option while initializing the datatables to provide caching.

var cache = new Object();
var rowsToCache = 2000; //Minimum Cache size >(must) 2 * Maximum Display Length * Number of Navigation Button (Avoids false hits while traversing distant pages), Default Assumption: 2000 > 2 * 100 * 5
//Intelligient to fetch only unfetched data. Maintains cache window to left(rowsToCache/2) and right(rowsToCache/2) of current position.

var fnJSManagedCacheServer =
function ( sSource, aoData, fnCallback ) {
//Get the required variable values
var _tableDispLength;
var _indexDisplayStart;
var _searchData;
var sEcho;
var _useCache = true;
var _bIsSearch = false;

for(var i in aoData){
var d = aoData[i];
if(d.name == 'iDisplayLength') {
_tableDispLength = d.value;
}
if(d.name == 'iDisplayStart') {
_indexDisplayStart = d.value;
}
if(d.name == 'sSearch'){
_searchData = d.value;
}
if(d.name == 'sEcho'){
sEcho = d.value;
}
}
//Process all conditions that may result in a true call and not access cache
//Find if the call is result of only pagination and not because of search
if(_searchData != undefined && _searchData != ""){
_useCache = false;
_bIsSearch = true;
}
//If cache doesnot exist
if(cache['aaDataServer'] == undefined){
_useCache = false;
}
//If _useCache is still true, try to get data from the cache, else allow call to server
//by setting _useCache to false
if(_useCache == true){
//Predict False hit
_useCache = false;

//Get data from cache and paint
//Does requested page end is lesser than or equal to cache endpage
if(_indexDisplayStart + _tableDispLength <= (cache.cached_indexDisplayStart + cache.realLength)
|| cache.realLength < cache.requestedLength){//Last page and no more data present at server
//Does requested _indexDisplayStart is greater than or equal to cache _indexDisplayStart
if(_indexDisplayStart >= (cache.cached_indexDisplayStart)){
//Data must exist in cache
var json = cache['jsonResult'];
var aaData = cache['aaDataServer'].slice(Math.abs(cache.cached_indexDisplayStart - _indexDisplayStart), Math.abs(cache.cached_indexDisplayStart - _indexDisplayStart) + _tableDispLength < cache['aaDataServer'].length ? Math.abs(cache.cached_indexDisplayStart - _indexDisplayStart) + _tableDispLength : cache['aaDataServer'].length);
json.aaData = aaData;
json.sEcho = sEcho;
fnCallback(json);
//Cache hit
_useCache = true;
}
}
}

if(_useCache == false){

//Get rowsToCache/2 from existing cache and save
var _prevCacheData = [];
var _movingRight = true;
if(cache.cached_indexDisplayStart != undefined){
if(_indexDisplayStart > cache.cached_indexDisplayStart){
_movingRight = true;
}else{
_movingRight = false;
}
if(_indexDisplayStart > cache.cached_indexDisplayStart
&& _indexDisplayStart == (cache.cached_indexDisplayStart + cache['aaDataServer'].length)){
_prevCacheData = cache['aaDataServer'].slice(cache['aaDataServer'].length - rowsToCache/2);
}else if(_indexDisplayStart <= cache.cached_indexDisplayStart
&& _indexDisplayStart == (cache.cached_indexDisplayStart - _tableDispLength)){
_prevCacheData = cache['aaDataServer'].slice(0, cache['aaDataServer'].length == rowsToCache ? rowsToCache/2 : 0);
}
}
//Invalidate and re-init cache
cache = new Object();
cache._tableDispLength = _tableDispLength;
cache._indexDisplayStart = _indexDisplayStart;
cache['aaDataServer'] = _prevCacheData;
cache._movingRight = _movingRight;

if(_bIsSearch != true){
//Modify settings of datatables to fetch rowsToCache records
cache.requestedLength = 0;
var _indexDisplayStartIsZero = false;
for(var i in aoData){
var d = aoData[i];
if(d.name == 'iDisplayStart') {
cache.requestedLength += d.value - (rowsToCache/2) >= 0 ? rowsToCache/2 : 0; //Order of these stmts important
if(cache._movingRight == false){
d.value = (d.value+_tableDispLength) - (rowsToCache/2) >= 0 ? (d.value+_tableDispLength) - (rowsToCache/2) : 0; //Shift _indexDisplayStart to left by half cache page size (adding _tableDispLength to get the _indexDisplayStart to original location before taking it forward, else we will miss a page)
cache.cached_indexDisplayStart = d.value;
}else{
cache.cached_indexDisplayStart = d.value - _prevCacheData.length;
}
}
}
for(var i in aoData){
var d = aoData[i];
if(d.name == 'iDisplayLength') {
d.value = cache.cached_indexDisplayStart != 0 ? rowsToCache/2 : rowsToCache/2;
cache.requestedLength = d.value; //Adjust cache fetch requested
}
}
}
cache._bIsSearch = _bIsSearch;
//Use ajax to Call
$.ajax( {
"dataType": 'json',
"type": "POST",
"url": sSource,
"data": aoData,
"success": function(json) {
//Cache
cache['jsonResult'] = json;
if(cache._movingRight == true){
cache['aaDataServer'] = cache['aaDataServer'].concat(json.aaData.slice(0));//append entire aaData to right of existing cached data
}else{
cache['aaDataServer'] = (json.aaData.slice(0)).concat(cache['aaDataServer']);//append entire aaData to left of existing cached data
}
cache.realLength = cache['aaDataServer'].length;
if(cache._bIsSearch != true){
//redraw a part of json, as requested between 0 and current pagesize(cache._tableDispLength)
json.aaData = cache['aaDataServer'].slice(Math.abs(cache.cached_indexDisplayStart - cache._indexDisplayStart), cache.realLength < Math.abs(cache.cached_indexDisplayStart - cache._indexDisplayStart) + cache._tableDispLength ? cache.realLength : Math.abs(cache.cached_indexDisplayStart - cache._indexDisplayStart) + cache._tableDispLength);
}
fnCallback(json);
}
} );
}
}

To use this, just include the javascript, override “rowsToCache” if default is not appropriate for you, and set “fnServerData” : fnJSManagedCacheServer.

Do let me know if you find any bugs!

PS: No caching when searching.