BreezeJS - One query to rule them all and with the data bind them

Recently we've started working with BreezeJS in a SPA that will essentially serve as a kiosk application. As a discrete part of getting everything to run as smoothly as possible we need to cache data fetched via our Breeze controllers. We've taken the necessary steps to build in the cache-warming prefetch mechanism, but we're left with a rather clunky way of either executing a remote query in BreezeJS or executing a local query. This is due to a couple of things:

  1. Local queries are synchronous whereas remote queries use a promise pattern
  2. There is no intelligence to know whether the local data one is querying is complete enough to trust

Given those complications I wanted to drive consistency by providing a mechanism by which a developer could make a single call that followed a single pattern and at the same time have that call be intelligent enough to make a decision on whether to execute that query locally or remotely. Most of the calls back to the server in this case are rather simple and straight forward have at most a couple of where predicates and some expanded collections within the domain model. Thus, it wasn't too hard to build in that single call to rule them all and add some tracking and conditional query logic.

 Converging on a Single Method Call

First, let's take a look at the problem presented by wanting to query both locally and remotely using BreezeJS. A query in BreezeJS with a predicate looks something like this:

       var query = EntityQuery.from('bodyStyle')

            .where("model.modelYear","equals", Number(year))

            .where("model.modelId","equals", Number(model))

            .expand("model");

 

      return manager.executeQuery(query)

            .then(querySucceeded)

            .fail(queryFailed);

 

This in itself presents no issues.  The EntityManager.executeQuery() function returns a promise and we provide the callback handlers for the promise resolution.  Where we start to get into trouble is when we want to query the local cache first.  In that case, we have to make a synchronous call that then looks more like this:

               

       var query = EntityQuery.from('bodyStyle')

            .where("model.modelYear","equals", Number(year))

            .where("model.modelId","equals", Number(model))

            .expand("model");

 

localQuery= query.toType(targetTypeName);

                // local query

       returnthis.executeQueryLocally(localQuery);

 

 

 

So, if I want to check the local cache first and then follow up with a remote query in the case that I don’t have results I have to build logic around every call to make a synchronous cache query, check it, make remote query, and handle the promise.  In order to provide a consistent developer interface I want to converge on one style of call; either synchronous or asynchronous (promise pattern).  Additionally, I don’t want to put the single method into the dataContext object itself, because what I really want is that any developer working on a dataContext using BreezeJS will have the method available.  To accomplish this, I added a method to the breeze prototype that I named combinedQuery.  This method would always return a promise making the dataContext developer interaction with server and local data consistent which in turn flowed through to the viewModel developer as he works with a given dataContext.

Skipping over the parts on which I’m not focused, I want to point out the first part of providing the promise pattern style is that I I’m using a deferred object to provide a promise to the caller in any case.  This is done simply enough by instantiating a deferred object at the top of the function and then returning the promise of the deferred object at the bottom of the function.

breeze.EntityManager.prototype.combinedQuery=function(query, remoteOnly){

    var deferred = Q.defer();

    var results =null;

    var localQuery;

 

    //default value for parameters

    if(remoteOnly===null|| remoteOnly === undefined)

    {

        remoteOnly =false;

    }

 

    try{

        if(!remoteOnly){

            if(queryTracker.exists(query)){

                localQuery = query.toType(query.resultEntityType);

                //try local query

                results =this.executeQueryLocally(localQuery);

            }

        }

        //if it isn't in cache, results should be null

        if(results !=null&& results != undefined && results.length >0){

            deferredSuccess(results);

        }

        else//execute remote

        {

            queryTracker.add(query);

            this.executeQuery(query)

                .then(deferredSuccess)

                .fail(deferredFailure);

        }

    }

    catch(e){

        deferredFailure(e);

    }

 

 

    function deferredSuccess(data)…

    function deferredFailure(data)…

 

    return deferred.promise;

};

1. combinedQuery() complete

I first try the local query and check the results to see if I received any.  If I do, I assume that they are the desired results and return then as part of the deferred object’s success handler for the promise:

        if(results !=null&& results != undefined && results.length >0){

            deferredSuccess(results);

        }

2. Check results for local query

If I didn’t get any results then I try the remote query and pass the deferred object’s success handler.  Since I’m returning the promise of the deferred object, the promise is resolved by either the local or remote query in the deferredSuccess() function:

    function deferredSuccess(data){

        var returnData;

 

        if(data.results=== undefined)

        { returnData = data;}

        else

        { returnData = data.results;}

 

        deferred.resolve(returnData);

    }

3. Returning only data to the promise resolve

One last adjustment that must be made in order to return a consistent result to the caller is to return the data.  Using a remote query, the result contains information about the query and other things including a property named ‘results’.  In order for me to get at the data from the remote query I have to access that property.  However, for a local query, the data passed to the success handler is only the resulting data.  Thus, in order to just have my caller get the results each time I’m handling that in the success handler by resolving the promise with only the data results.

Matched Query Caching

With the combinedQuery() method in place, the developers have a consistent means to query data with worrying about its location.  However, I’m left with a problem.  The check I’m doing against the local query simply looks for any results.  The problem with that is if I query EntityA and get 1 result, but in a subsequent query against EntityA we expect 3 the combinedQuery would see the 1 result from the cache, declare success, and return the result.  Usually, the developer making the call would make a decision to make a remote call knowing that data in the cache was insufficient to address the second query.  Thus, I needed to bake in some minimal intelligence to match the query such that any query will be routed to the remote endpoint the first time it is requested, but will hit localcache the second time.

To do this, I created a queryTracker that does an exists check.  If the query has not been made before the combinedQuery() will make a remote call and add the query to the list of queries that have been called.  If it does exists, I check the local cache first.

            if(queryTracker.exists(query)){

                localQuery = query.toType(query.resultEntityType);

                //try local query

                results =this.executeQueryLocally(localQuery);

            }

4. Check for previous query execution then query locally

Within the queryTracker I need to do some sort of deep compare.  However, knowing how we’re querying, I don’t go for a full deep compare, but instead simply filter to match the resultEntityType and then compare the predicates of each one of those to the predicates on the new query.

    function queryExists(query)

    {

       

        //check for existing query based on target entity and predicate

        var matchedQueries =new Array();

        matchedQueries = _.filter(queries,function(savedQuery){

            return savedQuery.resultEntityType == query.resultEntityType &&

                    savedQuery.wherePredicate.toString()== query.wherePredicate.toString();});

 

        return matchedQueries.length >0;

     }

5. queryExists function

The simplest way I found to do this was to make use of a toString() function that is available on the wherePredicate object of the query.  Fortunately, it serializes the complete list of predicates and seems to do it in the same order each time.  This has obvious limitations in that I don’t account for query differences like what is expanded or navigation property queries.  However, for our use it is sufficient; for yours you may need a deeper/more complex comparison between query objects.

Summary

With that, we have created a single promise based query mechanism that shows up as part of the EntityManager object and makes decisions about local and remote querying based on a query match for entity type and predicates.  Hopefully, you’ll find some part of this useful in the code you’re writing using Breeze.  I’ve included the full examples below for reference.

var breezeQueryTracker =function(){

 

    var queries =[];

 

  function addQuery(queryToAdd)

    {

        queries.push(queryToAdd);

    }

 

    function removeQuery(queryToRemove)

    {

        //TODO: Implementation

        returnfalse;

    }

 

    function queryExists(query)

    {

       

        //check for existing query based on target entity and predicate

        var matchedQueries =new Array();

        matchedQueries = _.filter(queries,function(savedQuery){

            return savedQuery.resultEntityType == query.resultEntityType &&

        savedQuery.wherePredicate.toString()== query.wherePredicate.toString();});

 

        return matchedQueries.length >0;

     }

   

 

    return{

        queries: queries,

        add: addQuery,

        //remove: removeQuery,

        exists: queryExists

    };

}

 

var queryTracker =new breezeQueryTracker();

 

//add query command to breeze

breeze.EntityManager.prototype.combinedQuery=function(query, targetTypeName, remoteOnly){

    var deferred = Q.defer();

    var results =null;

    var localQuery;

 

    //default value for parameters

    if(remoteOnly===null|| remoteOnly === undefined)

    {

        remoteOnly =false;

    }

 

    try{

        if(!remoteOnly){

            if(queryTracker.exists(query)){

                localQuery = query.toType(targetTypeName);

                //try local query

                results =this.executeQueryLocally(localQuery);

            }

        }

        //if it isn't in cache, results should be null

        if(results !=null&& results != undefined && results.length >0){

            deferredSuccess(results);

        }

        else//execute remote

        {

            queryTracker.add(query);

            this.executeQuery(query)

                .then(deferredSuccess)

                .fail(deferredFailure);

        }

    }

    catch(e){

        deferredFailure(e);

    }

 

 

    function deferredSuccess(data){

        var returnData;

        //if the data we went after was in a remote store, the return value has the associated query, entitymanager, and other objects.

        //however, we only want the data.results.

        //if it was a local query then the incoming data will be all that we need.

        if(data.results=== undefined)

        { returnData = data;}

        else

        { returnData = data.results;}

 

        deferred.resolve(returnData);

    }

    function deferredFailure(data){

        deferred.reject(data);

    }

 

    return deferred.promise;

};