Many questions pop up in the Couchbase forum and Stack Overflow regarding reading and writing only parts of a document rather than the full document. I can see where this would be a concern. For example, what if you have documents that are megabytes in size? Transferring these large documents between the application layer and Couchbase Server can become quite slow.

This is where the Couchbase sub-document API, part of Couchbase Server 4.5, comes into play. Instead of getting a full and potentially large document based on a key, you can get only the parts of the document that you need. The same can be said about writing changes back into Couchbase. For example, what if you have a document with user profile data and you only need to change the address? Wouldn't it make more sense to only write that change?

We're going to take a look at using the sub-document API in a Node.js application using the Node.js SDK for Couchbase. Before we start doing that, let's come up with a story. Let's say we have a user profile with social media data. We're going to base our manipulations only on the social media data, not the rest of the document. So for example:

{ firstName: "Nic", lastName: "Raboy", socialNetworking: { twitter: "nraboy" } } 1 2 3 4 5 6 7 8 9 { firstName : "Nic" , lastName : "Raboy" , socialNetworking : { twitter : "nraboy" } }

The above JSON document will be the base document that we work with.

To keep things easy to understand, let's create a fresh Node.js project to work with. We will build upon this new project for clarity. Assuming Node.js is installed and you have Couchbase Server 4.5+ running somewhere, execute the following from your Command Prompt or Terminal:

npm init -y npm install couchbase --save 1 2 3 4 npm init - y npm install couchbase -- save

You should probably create a new directory before executing the above commands, but they will essentially create a new Node.js project and install the Couchbase Node.js SDK. Create an app.js file in the same directory as it will house our logic.

Going forward we're going to spend our time in the app.js file. Open it and let's add the basic bootstrapping code to connect to Couchbase.

var Couchbase = require("couchbase"); var cluster = new Couchbase.Cluster("couchbase://localhost"); var bucket = cluster.openBucket("default"); 1 2 3 4 5 6 var Couchbase = require ( "couchbase" ) ; var cluster = new Couchbase . Cluster ( "couchbase://localhost" ) ; var bucket = cluster . openBucket ( "default" ) ;

In the above we're connecting to a locally running Couchbase instance and opening the default bucket. Feel free to change this as appropriate. Now we're going to proceed to creating the following four JavaScript functions:

var createDocument = function(documentId, document) { } var getSubDocument = function(documentId) { } var upsertSubDocument = function(documentId) { } var getDocument = function(documentId, isFinished) { } 1 2 3 4 5 6 var createDocument = function ( documentId , document ) { } var getSubDocument = function ( documentId ) { } var upsertSubDocument = function ( documentId ) { } var getDocument = function ( documentId , isFinished ) { }

We are creating four functions to prevent a giant nested chain of asynchronous function callbacks. Since our project will be designed more like a script, a chain can get a little difficult to follow.

Let's go down the line with each of these functions in order. Starting with the createDocument function, it will look something like this:

var createDocument = function(documentId, document) { bucket.upsert(documentId, document, (error, result) => { if(error) { return console.log("ERROR: ", error); } getSubDocument(documentId); }); } 1 2 3 4 5 6 7 8 9 10 var createDocument = function ( documentId , document ) { bucket . upsert ( documentId , document , ( error , result ) = > { if ( error ) { return console . log ( "ERROR: " , error ) ; } getSubDocument ( documentId ) ; } ) ; }

The above function assumes we are passing a document key and a JSON document. Don't worry, we'll see this at the end. What is happening though is we are doing an upsert to create a full new document. We're essentially using this function as our initialization function for this example. When the full profile document is created, it will call the next function when successful. This next function is the getSubDocument function and it looks like the following:

var getSubDocument = function(documentId) { bucket.lookupIn(documentId).get("socialNetworking").execute((error, result) => { if(error) { throw error; } upsertSubDocument(documentId); }); } 1 2 3 4 5 6 7 8 9 10 var getSubDocument = function ( documentId ) { bucket . lookupIn ( documentId ) . get ( "socialNetworking" ) . execute ( ( error , result ) = > { if ( error ) { throw error ; } upsertSubDocument ( documentId ) ; } ) ; }

The above function will take the initial document key that we define and declare that we're going to do a lookup rather than getting a document. When doing our lookup we are looking for the socialNetworking property, then we execute this lookup. We can get more than one property if we want, because the execution result will return an array of properties. For example, the array of the result contents above would look like this:

[ { id: 0, value: { twitter: 'nraboy' }, path: 'socialNetworking' } ] 1 2 3 [ { id : 0 , value : { twitter : ' nraboy ' } , path : ' socialNetworking ' } ]

After the getSubDocument function completes successfully, it calls the upsertSubDocument function that allows us to change a part of the document without first obtaining the full document, or even part of it.

var upsertSubDocument = function(documentId) { bucket.mutateIn(documentId, 0, 0).upsert("socialNetworking.website", "thepolyglotdeveloper.com", true).execute((error, result) => { if(error) { throw error; } getDocument(documentId); }); } 1 2 3 4 5 6 7 8 9 10 var upsertSubDocument = function ( documentId ) { bucket . mutateIn ( documentId , 0 , 0 ) . upsert ( "socialNetworking.website" , "thepolyglotdeveloper.com" , true ) . execute ( ( error , result ) = > { if ( error ) { throw error ; } getDocument ( documentId ) ; } ) ; }

Let's be clear here. When we are using the mutateIn method above, we are not first obtaining the document. It acts kind of like the lookup we did earlier. We are telling Couchbase which document we plan to change based on the key, then we are upserting a particular property. In the example above, we are trying to create a new property called website. It is a nested property so we are passing in the full path along with the value of said property. Then we can execute the request.

Finally, we want to see the full document that we are left with. Assuming the mutation was successful, the getDocument function will be called. It will look like the following:

var getDocument = function(documentId) { bucket.get(documentId, (error, result) => { if(error) { throw error; } console.log(result); }); } 1 2 3 4 5 6 7 8 9 10 var getDocument = function ( documentId ) { bucket . get ( documentId , ( error , result ) = > { if ( error ) { throw error ; } console . log ( result ) ; } ) ; }

In the above function we are just doing a standard get by document key. To finish things off, it is probably a good idea to share the piece of code that called our first function. It can be seen as follows:

createDocument("nraboy", { firstName: "Nic", lastName: "Raboy", socialNetworking: { twitter: "nraboy" } }); 1 2 3 4 5 6 7 8 9 createDocument ( "nraboy" , { firstName : "Nic" , lastName : "Raboy" , socialNetworking : { twitter : "nraboy" } } ) ;

Conclusion

The Couchbase sub-document API is incredibly beneficial when working with larger documents. You can not only obtain pieces from a document to reduce the transfer size, but you can also write pieces. I shared only some of what you can do with the API for Node.js. For another example, visit this project on GitHub. Further commands can be seen in the Node.js SDK documentation.