
MongoDB and the Go language
mgo is a popular MongoDB driver written in the Go language. The package page can be found at: http://labix.org/mgo. The driver is nothing more than a number of Go packages that facilitate writing Go programs capable of working with MongoDB.
In order to make use of mgo, the first step is to make use of the go get command to retrieve the package:
go get gopkg.in/mgo.v2
With the preceding command executed, we get the ability to use mgo in our code. We'd need to import the mgo package and the bson package that we discussed earlier. The package name we use to host our MongoDB persistence layer is called mongolayer.
Let's have a look at the mongolayer package:
package mongolayer
import (
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
Next, let's create some constants to represent the name of our database and the names of the collections involved in our persistence layer. The database name in MongoDB would be myevents. The collection names we'll use are users for the users collection, and events for the collection of events in our database:
const (
DB = "myevents"
USERS = "users"
EVENTS = "events"
)
In order to expose the features of the mgo package, we will need to utilize a database session object that belongs to the mgo package, the session object type is called *mgo.session. To make use of *mgo.session inside our code, we will wrap it with a struct type called MongoDBLayer, as follows:
type MongoDBLayer struct {
session *mgo.Session
}
It is now time to implement the DatabaseHandler interface that we covered earlier in order to construct the concrete persistence layer of our application. In the Go language, it is typically preferred to use a pointer type when implementing an interface because pointers preserve references to the original memory addresses of the underlying objects as opposed to copying the entire object around when we make use of it. In other words, the implementer object type for the DatabaseHandler interface needs to be a pointer to a MongoDBLayer struct object, or just simply *MongoDBLayer.
However, before we start implementing the interface, we will first need to create a constructor function that returns an object of the *MongoDBLayer type. This is idiomatic in Go in order for us to be able to execute any necessary initialization code while creating a new object of the *MongoDBLayer type. In our cases, the initialization code is basically to obtain a connection session handler to the desired MongoDB database address. The following is what the constructor code will look like:
func NewMongoDBLayer(connection string) (*MongoDBLayer, error) {
s, err := mgo.Dial(connection)
if err!= nil{
return nil,err
}
return &MongoDBLayer{
session: s,
}, err
}
In the preceding code, we created a constructor function called NewMongoDBLayer, which requires a single argument of type string. The argument represents the connection string with the information needed to establish the connection to the MongoDB database. According to mgo documentation at https://godoc.org/gopkg.in/mgo.v2#Dial, the format of the connection string needs to look like this:

If it is just a localhost connection, the connection string will look as follows: mongodb://127.0.0.1
If a port number is not provided in the connection string, the port defaults to 27017.
Now, let's look at the code inside our constructor function. In the first line, we call mgo.Dial() with the connection string as an argument. The mgo.Dial() is the function in the mgo package, which will return a MongoDB connection session for us to use later in our code. It returns two results—the *mgo.Session object and an error object. We use struct literals at the end to return a fresh object of type pointer to MongoDBLayer, which hosts the newly created *mgo.Session object. We also return the error object so that we communicate to the caller any errors that would have occurred during the initialization process.
Now, with the constructor out of the way, it's time to implement the methods of the DatabaseHandler interface. So far, we have four methods—AddEvent(Event), FindEvent([]byte), FindEventByName(string), and FindAllAvailableEvents().
Here is what the code for the AddEvent(Event) method will look like:
func (mgoLayer *MongoDBLayer) AddEvent(e persistence.Event) ([]byte, error) {
s := mgoLayer.getFreshSession()
defer s.Close()
if !e.ID.Valid() {
e.ID = bson.NewObjectId()
}
//let's assume the method below checks if the ID is valid for the location object of the event
if !e.Location.ID.Valid() {
e.Location.ID = bson.NewObjectId()
}
return []byte(e.ID), s.DB(DB).C(EVENTS).Insert(e)
}
The method takes an argument of type persistence.Event, which models the information expected from an event as we covered earlier. It returns a slice of bytes, which represents the event ID, and an error object, which will be nil if no errors are found.
In the first line, we call a getFreshSession() method—this is a helper method implemented in our code to help retrieve a fresh database session from the connection pool. Here is what the method code looks like:
func (mgoLayer *MongoDBLayer) getFreshSession() *mgo.Session {
return mgoLayer.session.Copy()
}
The session.Copy() is the method that is called whenever we are requesting a new session from the mgo package connection pool. mgoLayer.session here is basically the *mgo.Session object we have hosted inside the MongoDBLayer struct. It is idiomatic to call session.Copy() at the beginning of any method or function that is about to issue queries or commands to MongoDB via the mgo package. The getFreshSession() method is just a helper method that calls session.Copy() for us and returns the resulting session.
Now, let's return to the AddEvent() method. We now have a working *mgo.Session object from the database connection pool to use in our code. The first thing to do is to call defer s.Close() to ensure that this session gets returned back to the mgo database connection pool after the AddEvent() method exits.
Next, we check whether the event ID supplied by the Event argument object is valid and whether the ID field of the Event object is of the bson.ObjectID type as we covered earlier. bson.ObjectID supports a Valid() method, which we can use to detect whether the ID is a valid MongoDB document ID or not. If the supplied event ID is not valid, we will create one of our own using the bson.NewObjectID() function call. We will then repeat the same pattern with the location-embedded object inside the event.
Finally, at the end, we will return two results—the first result is the event ID of the added event, and a second result is an error object representing the result of the event insertion operation. In order to insert the event object to the MongoDB database, we will use the session object in the s variable, then call s.DB(DB).C(EVENTS) to obtain an object that represents our events collection in the database. The object will be of the *mgo.Collection type. The DB() method helps us access the database; we will give it the DB constant as an argument, which has our database name. The C() method helps us access the collection; we will give it the EVENTS constant, which has the name of our events collection.
The DB and EVENTS constants were defined earlier in our code. Then, finally, we will call the Insert() method of the collection object, with the Event object as an argument, which is why the code ends up looking like this—s.DB(DB).C(EVENTS).Insert(e). This line is what we need in order to insert a new document into a MongoDB database collection that utilizes Go objects and the mgo package.
Now, let's look at the code for FindEvent(), which we use to retrieve the information of a certain event from the database from its ID. The code will look as follows:
func (mgoLayer *MongoDBLayer) FindEvent(id []byte) (persistence.Event, error) {
s := mgoLayer.getFreshSession()
defer s.Close()
e := persistence.Event{}
err := s.DB(DB).C(EVENTS).FindId(bson.ObjectId(id)).One(&e)
return e, err
}
Note how the ID is passed as a slice of bytes instead of a bson.ObjectId type. We do this to ensure that the FindEvent() method in the DatabaseHandler interface stays as generic as possible. For example, we know that in the world of MongoDB, the ID will be of the bson.ObjectId type, but what if we now want to implement a MySQL database layer? It would not make sense to have the ID argument type passed to FindEvent() as bson.ObjectId. So, that's why we picked the []byte type to represent our ID argument. In theory, we should be able to convert a slice of bytes to any other type that can represent an id.
An important remark is that we could also have picked the empty interface type (interface{}), which in Go can be converted to any other type.
In the first line of the FindEvent() method, we obtained a fresh session from the connection pool using the mgoLayer.getFreshSession() as before. We then call defer s.Close() to ensure that the session goes back to the connection pool after we are done.
Next, we created an empty event object e using the code e := persistence.Event{}. We then use s.DB(DB).C(EVENTS) to access the events collection in MongoDB. There is a method called FindId(), which is supported by *mgoCollection objects of mgo. The method takes an object of the bson.ObjectId type as an argument then searches for the document with the desired ID.
FindId() returns an object of the *mgo.Query type, which is a common type in mgo that we can use to retrieve results of queries. In order to feed the retrieved document data to the e object we created earlier, we will need to call the One() method, which belongs to the *mgo.Query type, and pass a reference to e as an argument. By doing this, e will obtain the data of the retrieved document with the desired ID. If the operation fails, the One() method will return an error object containing the error information, otherwise One() will return nil.
At the end of the FindEvent() method, we will return the event object and the error object.
Now, let's look at the implementation of the FindEventByName() method, which retrieves an event by its name from the MongoDB database. Here is what the code looks like:
func (mgoLayer *MongoDBLayer) FindEventByName(name string) (persistence.Event, error) {
s := mgoLayer.getFreshSession()
defer s.Close()
e := persistence.Event{}
err := s.DB(DB).C(EVENTS).Find(bson.M{"name": name}).One(&e)
return e, err
}
The method is very similar to the FindEvent() method, except for two things. The first difference is the fact that FindEvent() takes a string as an argument, which represents the event name that we would like to find.
The second difference is that we query for an event name instead of an event ID. The code line where we query the document uses a method called Find() instead of FindId(), which makes the code look like this:
err := s.DB(DB).C(EVENTS).Find(bson.M{"name":name}).One(&e)
The Find() method takes an argument that represents the query we would like to pass along to MongoDB. The bson package provides a nice type called bson.M, which is basically a map we can use to represent the query parameters that we would like to look for. In our case, we are looking for the name that got passed as an argument to FindEventByName. The name field in the event collection in our database is simply coded as name, whereas the variable that got passed to us as an argument and has the name is called name. Hence, our query ends up as bson.M{"name":name}.
Last but not least is our FindAllAvailableEvents() method. The method returns all available events in our database. In other words, it returns the entire events collection from our MongoDB database. Here is what the code looks like:
func (mgoLayer *MongoDBLayer) FindAllAvailableEvents() ([]persistence.Event, error) {
s := mgoLayer.getFreshSession()
defer s.Close()
events := []persistence.Event{}
err := s.DB(DB).C(EVENTS).Find(nil).All(&events)
return events, err
}
The code is almost the same as FindEventByName(), except for three simple differences. The first difference is obviously the fact that FindAllAvailableEvents() doesn't take any arguments.
The second difference is the fact that we need the query results to be fed to a slice of the event's objects instead of a single event object. This is why the return type is []persistence.Event , instead of just persistence.Event.
The third difference is that the Find() method will take an argument of nil instead of a bson.M object. This will cause the code to look like this:
err := s.DB(DB).C(EVENTS).Find(nil).All(&events)
When the Find() method gets a nil argument, it will return everything found in the associated MongoDB collection. Also, note that we used All() instead of One() after Find(). That is because we expect multiple results and not just one.
With this, we finish covering our persistence layer.