Not too long ago I wrote about working with parts, or fragments, of documents in Couchbase
using the Node.js SDK. Being able to work with
parts of documents is made possible using
Couchbase Server 4.5 and higher and the sub-document API.
This is huge because when working with NoSQL documents you may find yourself with very large documents due to all the embedded JSON data. As you probably
know, making requests on large documents is slow, and in the modern web age, everything needs to be fast. Instead, it is more efficient to only
work with what you need and not everything all at once.
This time around we’re going to look at doing the same NoSQL document manipulations we saw in Node.js, but this time with the Go programming language. Let’s
come up with a data story for this example. It will be the same data story as the previous example, but let’s assume we have the following JSON document:
1 2 3 4 5 6 7 8 9 |
{ firstName: "Nic", lastName: "Raboy", socialNetworking: { twitter: "nraboy" } } |
The above data will be a basic user profile store with social media information. All of our manipulations will be around the social media information, not
the parent data that surrounds it.
To keep things simple for this guide, we are going to work with a fresh project. At this point we’ll assume you have both Couchbase Server 4.5+ and
GoLang installed and configured on your machine. If you’ve not already downloaded the GoLang SDK for Couchbase, execute the following from your Command
Prompt or Terminal:
1 2 3 |
go get github.com/couchbase/gocb |
Our entire project for this example will reside in a single file. We’re going to refer to this file as main.go and it can reside in any
Go project directory you want as long as it meets the requirements of the Go programming language.
To start things off, let’s create a main
function inside our project:
1 2 3 4 5 6 7 8 9 |
func main() { fmt.Println("Starting the app...") cluster, _ := gocb.Connect("couchbase://localhost") bucket, _ = cluster.OpenBucket("default", "") person := Person{FirstName: "Nic", LastName: "Raboy", SocialNetworking: &SocialNetworking{Twitter: "nraboy"}} createDocument("nraboy", &person) } |
There are a few things to note in the above. First we are establishing a connection to a locally running Couchbase cluster. When the connection has
been established we open the default
bucket. Notice that we are only assigning a value to the bucket
and not defining it. This
is because we are going to use this variable globally and must define it outside the main
function. With the bucket open, we are going to
create our initial data structure. This data structure Person
is defined below:
1 2 3 4 5 6 7 8 9 10 11 12 |
type Person struct { FirstName string `json:"firstname,omitempty"` LastName string `json:"lastname,omitempty"` SocialNetworking *SocialNetworking `json:"socialNetworking,omitempty"` } type SocialNetworking struct { Twitter string `json:"twitter,omitempty"` Website string `json:"website,omitempty"` } |
The Person
structure will have the basic user information and reference another structure called SocialNetworking
. Both
structures are tagged with JSON property names that are to be excluded from print if they are blank.
Going back to the main
function. Notice that our new person object is missing the website. We’re going to be adding this later. The first
function we call from the main
function is called createDocument
and it will add our object to the database. This function
is defined as follows:
1 2 3 4 5 6 7 8 9 10 11 12 |
func createDocument(documentId string, person *Person) { fmt.Println("Upserting a full document...") _, error := bucket.Upsert(documentId, person, 0) if error != nil { fmt.Println(error.Error()) return } getDocument(documentId) getSubDocument(documentId) } |
In the above function we are not yet working with fragments of a document. We need to start the example with fresh data first. We’re going to upsert
the intitial document and provided there are no errors we’re going to call getDocument
to validate it was created and then
getSubDocument
to get a certain part of the document. The getDocument
function will be used twice in this application and
it looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func getDocument(documentId string) { fmt.Println("Getting the full document by id...") var person Person _, error := bucket.Get(documentId, &person) if error != nil { fmt.Println(error.Error()) return } jsonPerson, _ := json.Marshal(&person) fmt.Println(string(jsonPerson)) } |
In the above getDocument
function we are getting the entire document based on id, marshalling it into JSON, and then printing it out. This
brings us to the getSubDocument
function as seen below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func getSubDocument(documentId string) { fmt.Println("Getting part of a document by id...") fragment, error := bucket.LookupIn(documentId).Get("socialNetworking").Execute() if error != nil { fmt.Println(error.Error()) return } var socialNetworking SocialNetworking fragment.Content("socialNetworking", &socialNetworking) jsonSocialNetworking, _ := json.Marshal(&socialNetworking) fmt.Println(string(jsonSocialNetworking)) upsertSubDocument(documentId, "thepolyglotdeveloper.com") } |
In the above getSubDocument
function we are doing a lookup within a document for a specific property. This is where we start working with
the sub-document API. The lookup we’re performing is a lookup for the socialNetworking
property. Notice that I am referring to the JSON, not
the struct
name. When we have the fragment we can marshal it into JSON and then print it out. The result should look like this:
1 2 3 4 5 |
{ "twitter": "nraboy" } |
At the end of the getSubDocument
function we make a call to a soon to be created upsertSubDocument
function. This is where
we are going to modify part of a document without first obtaining the entire document. This function can be seen as follows:
1 2 3 4 5 6 7 8 9 10 11 |
func upsertSubDocument(documentId string, website string) { fmt.Println("Upserting part of a document...") _, error := bucket.MutateIn(documentId, 0, 0).Upsert("socialNetworking.website", website, true).Execute() if error != nil { fmt.Println(error.Error()) return } getDocument(documentId) } |
In the above function we first specify which document we want to manipulate based on the document id. Then we say we want to perform an upsert on a
certain path or property of document. In this example we are saying we want to upsert a website
property found in the
socialNetworking
parent. Note that this entire process happens without actually obtaining the document.
When we’re done we do a full document lookup again to see what the document looks like as a whole. In case you need this to be put into perspective a
little better, the full code to this project can be seen below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
package main import ( "encoding/json" "fmt" "github.com/couchbase/gocb" ) var bucket *gocb.Bucket type Person struct { FirstName string `json:"firstname,omitempty"` LastName string `json:"lastname,omitempty"` SocialNetworking *SocialNetworking `json:"socialNetworking,omitempty"` } type SocialNetworking struct { Twitter string `json:"twitter,omitempty"` Website string `json:"website,omitempty"` } func getDocument(documentId string) { fmt.Println("Getting the full document by id...") var person Person _, error := bucket.Get(documentId, &person) if error != nil { fmt.Println(error.Error()) return } jsonPerson, _ := json.Marshal(&person) fmt.Println(string(jsonPerson)) } func createDocument(documentId string, person *Person) { fmt.Println("Upserting a full document...") _, error := bucket.Upsert(documentId, person, 0) if error != nil { fmt.Println(error.Error()) return } getDocument(documentId) getSubDocument(documentId) } func getSubDocument(documentId string) { fmt.Println("Getting part of a document by id...") fragment, error := bucket.LookupIn(documentId).Get("socialNetworking").Execute() if error != nil { fmt.Println(error.Error()) return } var socialNetworking SocialNetworking fragment.Content("socialNetworking", &socialNetworking) jsonSocialNetworking, _ := json.Marshal(&socialNetworking) fmt.Println(string(jsonSocialNetworking)) upsertSubDocument(documentId, "thepolyglotdeveloper.com") } func upsertSubDocument(documentId string, website string) { fmt.Println("Upserting part of a document...") _, error := bucket.MutateIn(documentId, 0, 0).Upsert("socialNetworking.website", website, true).Execute() if error != nil { fmt.Println(error.Error()) return } getDocument(documentId) } func main() { fmt.Println("Starting the app...") cluster, _ := gocb.Connect("couchbase://localhost") bucket, _ = cluster.OpenBucket("default", "") person := Person{FirstName: "Nic", LastName: "Raboy", SocialNetworking: &SocialNetworking{Twitter: "nraboy"}} createDocument("nraboy", &person) } |
Take this project for a test drive to see how wonderful the sub-document API is.
Conclusion
You just saw how to use the Couchbase Server Sub-Document API in a GoLang application using the Couchbase Go SDK. No longer will you have to worry about
passing around your potentially huge NoSQL documents, ruining your application response times. If you know your documents are large or you only need
bits and pieces, you can make use of this API.
For more information, visit the Couchbase Developer Portal.