Skip to content
mattdaly edited this page Apr 13, 2012 · 6 revisions

Sessions in node-ravendb are almost identical to those in the Raven C# client API in syntax, but behavioural differences exist. Node.js is event driven and node-ravendb is asynchronous and callback based. That means the results of some operation against your document store won't necessarily be available to the next call you make. The data returned will be provided to the callback function once the event loop has completed, during that cycle you might also be making other calls. Your operations won't always complete in the order they are specified. If you aren't familiar with callback based development there are plenty of resources out there to get you up to speed.


To gain access to a session, call the openSession function on your Store instance.

var session = require('./store').openSession();

Contents


Callbacks

Most operation functions on the session object accept a range of required or optional parameters, the last parameter will always be a required callback function.

Callbacks are fired when the specified operation has completed. As mentioned above, that won't always be in the same order specified in your code, because of that, subsequent operations you need to perform on the data returned to your callback should be executed inside the callback itself.

var max = undefined;
session.load('dogs/max', function(error, result) {
  max = result;
  console.log(max.name); // Max
});

console.log(max.name); // undefined

In the example above we want to load a dog with the id 'dogs/max'. To do that we use the load function, passing in our key as well as a the required callback. In our callback we are returned two parameters, an error and the result - one of those will be undefined, depending on whether the operation succeeded (you should always check the error is undefined before performing operations on result - more on errors below).

We assign the value returned by the load to a variable called max, and log the name of max to the console. Inside our callback this will return 'Max'. However, the call to log max.name outside the callback will likely result in 'undefined' being logged, rather than max's name. This is because as soon as session.load is executed, node.js moves on the the next command which is console.log(max.name);, while this is happening the load function is being executed and the callback is waiting on node-ravendb to make the HTTP call to our RavenDB instance and return the result.

In most cases, by the time node.js reaches console.log(max.name), max is still undefined - the callback hasn't looped back with the result set yet.

There are alternatives to nesting callbacks, control flow libraries allow you to execute functions in order.

Errors

Each session operation returns different parameters to the callback, but the first parameter will always be an error object. If no error occurs during the operation the error object will be undefined. Everytime you perform an operation you should always check for an error in your callback function. You can then pass this error on to your GUI or web page to give the user more information on what occurred.

The error parameter is a standard node.js Error object, node-ravendb also attaches the HTTP response code that RavenDB returns, along with the error message it produced, as part of that object. The HTTP status code is accessible via the statusCode property, and the message available using the message property.

session.load('dogs/max', function(error, result) {
  if (error) {
    console.log('HTTP ' + error.statusCode + ': ' + error.message);
  } else {
    console.log('Max!');
    console.log(result);
  }
});

As we know from the Store documentation, we can also set global error handlers. Those handlers are passed down to each session and fired before callbacks are returned, that means handling errors inside your callback when you have a global handler (either 'error' or a specific http status handler) will handle it twice.

Like the Store object, we can also set global handlers on a by session basis. Adding a handler to a session for either 'error' or a specific HTTP status, when a handler already exists, will simply overwrite the original applied to the session.

var raven = require('./node-ravendb');
var store = new raven.Store({ host: '1.1.1.1', port: 1234, database: 'Foo' }).initialize();

var session = store.openSession();

session.on('409', function (error) {
  // notify my GUI using the 409 html page
  console.log(error.statusCode + ': ' + error.message);
});

Conventions

All session conventions are identical to those on both the Store and Type objects, and are accessed using Session.conventions.

Read the Store documentation to find out more.


Options

All options on a session object are accessed under the Session.options property. Currently the only option supported is UseOptimisticConcurrency, but support for disabling caching is forthcoming. Options set on a session will override global options inherited from the store.

Optimistic Concurrency

"Every document in RavenDB has a corresponding e-tag (entity tag). This e-tag is updated by RavenDB every time the document is changed". More information on Optimistic Concurrency and Etags can be found here.

By default optimistic concurrency is disabled - etags are ignored and changes are made to documents regardless of whether they have been edited between your load and subsequent save. To enable optimistic concurrency set Session.options.useOptimisticConcurrency to true.

session.options.useOptimisticConcurrency = true

Future updates to documents will ensure the etag values match before saving any changes. If the etag values do not match, no changes are made to the document and node-ravendb returns an error object to the callbacks error parameter and sets the statusCode property to HTTP 409.

Disable Caching

Forthcoming in a future release.


If you haven't read the documentation on Types in node-ravendb you can do so here, they are necessary for interacting with your document store and new documents cannot be stored if they are not a valid node-ravendb Document object.


Store

Storing documents is very simple and uses the same system used in the official RavenDB client api. You 'store' a document in the session but nothing is committed to the database until you make an explicit call to 'save', more on that below.

When you store a document, node-ravendb's session object adds it to an internal array. When you call the save function all stored documents are processed as a batch and saved to your database. By using this internal array of documents, node-ravendb allows you to store your documents one by one in your session and then saves them all in one single HTTP POST request. If you are saving four documents this results in just one HTTP POST, rather than four.

Storing is easy, just use the DocumentSession.store function. Store is the one function that doesn't require a callback parameter, you just pass in your document.

Note: The official client api can use HiLo key generation to generate keys for your documents, this occurs when you store a document, however HiLo keys are not currently supported in node-ravendb so keys are generated on save, not store. This is because when using the GUID key generation strategy we set the key as null, this tells RavenDB to generate a GUID internally and assign it, therefore documents using the GUID strategy can only have their Id value set when the call to save is made. The same is also true for the default strategy of auto incrementing integers.

This example assumes you have read the Document documentation and are familiar with the 'Dog' object example.

var max = new Dog('Max', 'Golden Retriever', 12);
session.store(max);

The session now has an internal reference to max and will push the document to RavenDB when save is called.

Note: Javascript is pass by reference, so we effectively get free change tracking. Any changes to documents after they are stored are still reflected in the sessions internal copy and as long as they are made before the call to save, will be pushed to the database.

Batching

There are often cases where we need to store several documents are once. We know the session doesn't push anything to RavenDB until we call save, so we can easily just keep using the store function to push individual documents to the session before we save. However node-ravendb also allows you to store a batch of documents in one go.

To store a batch of documents, just push them to an array and store that array in one call, node-ravendb will figure out the difference and add them each separately.

var batch = [];

var max = new Dog('Max', 'Golden Retriever', 12);
var jess = new Dog('Jess', 'German Shepherd', 7);
var billy = new Dog('Billy', 'Labrador', 2);

batch.push(max);
batch.push(jess);
batch.push(billy);

session.store(batch);

There isn't any difference between storing a batch or a single document, the same process occurs but sometimes batching is more convenient.


Save

The save function performs the same functionality that it does in the official client api. It obviously saves 'new' documents that have been stored (see above), it also saves documents that have been loaded via the load function (see below) that have been changed.

Note: Change tracking for loaded documents makes use of JavaScript's pass by reference nature, so any changes to loaded documents are reflected internally in the session, in much the same way they are for stored documents. However, node-ravendb needs to know which loaded documents to save and which not to, to do this it stores two copies of each document loaded - a cached version of it's state when it was loaded and a reference copy that tracks changes. The two are compared when save is called and documents with changes are pushed to the database. Comparing each document isn't ideal, but until/if node.js supports Object.watch there is no easier way to do changing tacking.

To save documents just use the DocumentSession.save function. This function accepts a callback function as the parameter, that itself takes two parameters - error and a list of keys. The keys parameter is an array of keys generated by RavenDB for each document saved. If only one document is saved the keys parameter is still an array.

session.save(function(error, keys) {
  if (!error) {
    console.log(keys);  // [ 'dogs/1', 'dogs/2', 'dogs/3' ]
  }
});

Often we aren't storing new documents, we're just saving loaded documents. With that in mind the save function does not require a callback function to be passed as a parameter. Though it is recommended to handle any errors.

session.save();  // we do lose our ability to know whether the save failed or succeeded

If UseOptimisticConcurrency is set to true the save function will compare the etag of each document before it saves to the database. If the etags do not match (e.g. the document has changed between the load and the save) the error object will be set and will return a HTTP 409 status.

session.save(function(error, keys) {
  if (error && error.statusCode === 409) {
    console.log('Concurrency conflict, etags did not match');
  }
});

Load

The DocumentSession.load function requires two parameters, the key of the document to load and a callback function. The callback function takes two parameters itself, an error and the returned document. Each loaded document is cached and change tracked. Subsequent loads for cached documents will not hit the database.

session.load('dogs/max', function(error, document) {
  if (!error) {
    console.log(document); // { name: 'Max' ... }
  }
});

Includes

It is also possible to use the includes functionality offered by the official client api. node-ravendb doesn't offer chaining like the client api, but you can pass the names of properties to use for includes as an optional second parameter to the load function. Like the client api this results in just one HTTP GET request.

Included documents are returned as an array of documents in third parameter in the callback.

session.load('dogs/max', 'puppy', function(error, document, includes) {
  if (!error) {
    console.log(document); // { name: 'Max' ... }
    console.log(includes); // [ { name: 'Max's Puppy' ... } ]
  }
});

In the example above we load 'dogs/max' and we tell node-ravendb to grab whatever document is referenced by the property 'puppy' on the document 'dogs/max' (e.g. max.puppy = 'dogs/jess' - node-ravendb loads 'dogs/jess' as well).

The includes parameter of the callback returns an extended, queryable array that provides Linq like functionality. That queryable array provides a helper function 'load' to load documents from the array by their id.

session.load('dogs/max', 'puppy', function(error, document, includes) {
  if (!error) {
    var max = document;
    console.log(max); // { name: 'Max' ... }
    
    var puppy = includes.load(max.puppy);
    console.log(puppy); // { name: 'Max's Puppy' ... }
  }
});

Alternatively, included documents are cached internally in the session and can be loaded normally using session.load. You can ignore the third parameter in this case. This syntax isn't ideal because of the callback nesting but it demonstrates the session's cache.

session.load('dogs/max', 'puppy', function(error, document) {
  if (!error) {
    var max = document;
    console.log(max); // { name: 'Max' ... }

    session.load(max.puppy, function(err, doc) {
      console.log(doc); // [ { name: 'Max's Puppy' ... } ]
    });
  }
});

It is also possible to specify more than one include, just pass in an array of property names instead.

session.load('dogs/max', [ 'puppy', 'mother' ], function(error, document, includes) {
  if (!error) {
    var max = document;
    console.log(max); // { name: 'Max' ... }
    
    var puppy = includes.load(max.puppy);
    console.log(puppy); // { name: 'Max's Puppy' ... }

    var mother = includes.load(max.mother);
    console.log(mother); // { name: 'Max's Mom' ... }
  }
});

Batching

Like includes, we can also batch document keys. Just put the keys you want loaded into an array and pass them to the load function. For batched loads, the second parameter of callback function is now the same queryable array used for included documents. You can use the load function on that too.

session.load([ 'dogs/max', 'dogs/jess' ], function(error, documents) {
  if (!error) {
    console.log(documents); // [ { name: 'Max' ... }, { name: 'Jess' ... } ]

    console.log(documents.load('dogs/max')); // { name: 'Max' ... }
    console.log(documents.load('dogs/jess')); // { name: 'Jess' ... }
  }
});

Includes work too, both single and multiple. RavenDB will check each document for each specified property name in the includes parameter and load all referenced documents. If 'Max' and 'Jess' both have referenced puppies, both puppies will be loaded.

session.load([ 'dogs/max', 'dogs/jess' ], 'puppy', function(error, documents, includes) {
  if (!error) {
    var max = documents.load('dogs/max');
    var jess = documents.load('dogs/jess');
    
    var maxsPuppy = includes.load(max.puppy);
    var jessPuppy = includes.load(jess.puppy);

    console.log(maxsPuppy); // { name: 'Max's Puppy' ... }
    console.log(jessPuppy); // { name: 'Jess' Puppy' ... }
  }
});

Note: Batched loads (multiple document keys) are NOT currently cached by node-ravendb.


Query

Querying allows you to retrieve documents based on properties other than their keys. node-ravendb supports two types of querying, dynamic queries and queries against indexes.

Note: There is also currently no support for paging.

Dynamic Querying

We don't have generics in JavaScript but dynamic querying offers two options. You can pass in a string version of the Raven-Clr-Type you specified when creating your document type, or you can pass in that document type itself.

The DocumentSession.query function requires three parameters - the document type as a string or the type itself, an object literal containing the properties and values you want to query for and a callback function. The callback function returns three objects, an error, a queryable array of documents retrieved and a statistics object. More information can be found on the statistics object below.

session.query('Dog', { breed: 'Labrador' }, function(error, documents, statistics) {
  if (!error) {
    console.log(documents); // [ { name: Billy ... }, { name: 'Russell' ... } ]
    console.log(statistics);
  }
});

Or passing in a type. Dog is the type we created in the Document wiki.

session.query(Dog, { breed: 'Labrador' }, function(error, documents, statistics) {
  if (!error) {
    console.log(documents); // [ { name: Billy ... }, { name: 'Russell' ... } ]
    console.log(statistics);
  }
});

Dynamic queries work the same as in RavenDB. RavenDB will create a temporary index for you, if that index gets used frequently enough, Raven stores it permanently.

Querying uses Lucene syntax, so making complex queries is as easy as specifying the property name and putting the lucene syntax as part of the value for that property. Consult the Lucene documentation for more information.

session.query('Dog', { age: '5 TO 12' }, function(error, docs, stats) {
  if (!error) {
    console.log(documents); // [ all dogs between the age of 5 and 12]
  }
});

Index Querying

Querying indexes requires an index to already exist. node-ravendb doesn't currently support creating indexes, but you can use the RavenDB studio to create them yourself.

Querying an index uses the same syntax as a dynamic index, but rather than passing in the type of document as the first parameter we pass in the name of the index. We use the DocumentSession.index function to make a query against an index.

session.index('DogsByBreed', { breed: 'Labrador' }, function(error, documents, statistics) {
  if (!error) {
    console.log(documents); // [ { name: Billy ... }, { name: 'Russell' ... } ]
    console.log(statistics.total); // 2
  }
});

This time we're querying the index DogsByBreed and passing in 'Labrador' to the breed property. You can only pass in properties/'fields' that are defined on the index. Non indexed fields will result in the query failing and the error object being set, containing an error message about those un-indexed fields. Property names are case sensitive

Statistics

The statistics object provides the same output as using 'out statistics' in the official client api. Properties available on the statistics object are:

  • total - the total number of results (always set)
  • stale - true if the result set contains stale results
  • index - the name of the index the query was performed against
  • timestamp - the timestamp of the last time the index documents were indexed

Specifications

You can also perform queries using particular specifications to allow you to sort and page your result set, or to only return certain properties via projections.

To provide specifications to your queries attach a third parameter to your dynamic or index queries, this parameter should be an object literal that takes four optional properties - projections, sort and start and pageSize.

session.query('Dog', { breed: 'Labrador' }, { projections: [ ... ], sort: '...', ... }, function(error, docs, stats) {
   ...
});

More on each below...

Projections

Sometimes you want to make a query but you only want certain properties from the documents you are querying. To do this we can use projections.

Projections in node-ravendb are supplied to queries under the projections property of the specifications options detailed above. Just pass in either a single property as a string, or an an array of the property names you want RavenDB to pull for you. RavenDB attaches the document id under the property __document_id, to allow you to associate the age if you need to.

For example, we want to get the ages of every dog whose breed is 'Labrador'

session.query('Dog', { breed: 'Labrador' }, { projections: 'age' }, function(error, docs, stats) {
  if (!error) {
    console.log(docs); // [ { __document_id: 'dogs/billy', age: 2 }, { __document_id: 'dogs.russell', age: 'Russell' } ]
  }
});

Or multiple properties:

session.query('Dog', { name: 'Billy' }, { projections: [ 'name', 'age' ] }, function(error, docs, stats) {
  if (!error) {
    console.log(docs); // [ { __document_id: 'dogs/billy', name: 'Billy', age: 2 } ]
  }
});

Sorting

We can also have RavenDB sort our result before it returns it to us (although this functionality is also provided by the queryable array).

To sort your result set we specify the sort parameter on the specifications object. Similar to projections we can specify a single property name as a string, or multiple property names in an array. To sort a property in reverse order just prefix it with a minus symbol.

Get all Labradors and sort by age.

session.query('Dog', { breed: 'Labrador' }, { sort: 'age' }, function(error, docs, stats) {
  if (!error) {
    console.log(docs); // ordered by age
  }
});

Or by age in descending order

session.query('Dog', { breed: 'Labrador' }, { sort: '-age' }, function(error, docs, stats) {
  if (!error) {
    console.log(docs); // ordered by age
  }
});

Paging

'When the number of results gets too big, we need to page through them.'. node-ravendb allows you to return your result set using paging, you just need to supply the position to start returning results from in the result set and the number of results to return per page. This is done by specifying the start and pageSize properties to the specifications object detailed earlier.

session.query('Dog', { breed: 'Labrador' }, { sort: 'age', start: 0, pageSize: 10 }, function(error, docs, stats) {
  if (!error) {
    console.log(docs); // ordered by age, starting from the first result found down to the 10th
  }
});

Delete

The delete function located using Session.delete requires the document you want to delete, this means you need to have it loaded (to delete by key/id see the Advanced section below). The callback function is optional and only takes one parameter, an error object. RavenDB doesn't distinguish between successful and failed deletes, just errors. So if a document doesn't exist the same status is returned by RavenDB. If other errors occur they are passed to the error object, use a callback to handle potential errors.

session.load('dogs/max', function(error, document) {
  if (!error) {
    session.delete(document);
  }
});

You can also delete a document by it's key value, without having to have loaded the document first. The function requires at least two parameters - the document key to delete, an optional etag value (requires useOptimisticConcurrency to be true) and a callback function, which, like the standard delete function is optional and simply returns an error parameter.

session.delete('dogs/max', function(error) {
  if (!error) {
    // let the UI/webpage know
  }
});

Using an etag:

session.delete('dogs/max', '00000000-0000-2a00-0000-000000000007', function(error) {
  if (!error) {
    // let the UI/webpage know
  }
});

If you don't care about errors (or are handling them globally), you just want to try to delete and forget, you can use the following.

session.delete('dogs/max');

It is also possible to batch delete, passing an array containing any of the following - valid documents, document keys (ignores useOptimisticConcurrency (unless you pass documents with etags set - if one etag check fails, all attempted deletes fail)) or object literals of the form { key: 'dogs/max', etag: undefined }.

var max = // loaded max

session.delete([ max, 'dogs/jess', { key: 'dogs/billy', etag: '00000000-0000-2a00-0000-000000000007' } ], function(error) {
  if (!error) {
    // let the UI/webpage know
  }
});

Exists

Session.exists allows you to ask RavenDB whether a specific document exists, by passing in a key/id. This function makes a HEAD request to RavenDB and returns only a true or false value, no data is retrieved.

session.exists('dogs/max', function(error, exists) {
  if (exists) console.log('Max exists!');
});

Patching

Patching allows you to apply a specific change to a document without loading it, just pass in the document key and the patch. The patching API provides a variety of functions all of which can be found in the RavenDB documentation, the syntax used by node-ravendb is identical to that listed in the documentation.

The Session.patch function takes four parameters, the document key, an array of patches, an optional etag and an optional callback function. The callback is optional and takes one parameter, an error. Like the two delete methods you don't have to use a callback but it is advised to handle errors. The patches parameter is an array of patches (a patch is a object literal) and is always an array, even if there is only one patch (this may change). If an etag is specified and the etags do not match, all patches will fail.

session.patch('dogs/max', [ { Type: 'Inc', Name: 'age', Value: 1 } ], function (error) {
  if (!error) {
    console.log('Happy Birthday Max!');
  }
});

Specifying an etag:

var birthday = { Type: 'Inc', Name: 'age', Value: 1 };
session.patch('dogs/max', [ birthday ], '00000000-0000-2a00-0000-000000000007', function (error) {
  if (!error) {
    console.log('Happy Birthday Max!');
  }
});

Field level concurrency is not currently supported.