No creo que a nadie le extrañe si decimos que Mongo DB es una de las bases de datos no relacionales más famosas que existen, por ello no podía faltar su adaptación a Go para poder utilizarla. Hasta hace poco contábamos con varias librerías para utilizar MongoDB, siendo una de las más utilizadas https://gopkg.in/mgo.v2, y si bien es cierto que era realmente útil no teníamos una solución oficial por parte del equipo de mongo para integrarse con nuestro lenguaje favorito.
Pero esto cambió recientemente, donde en marzo de este mismo año se presentaba la versión estable de este driver oficial para mongo.
Así que vamos a explicar cómo utilizar el driver oficial, para qué podáis utilizar MongoDB en aquellos proyectos que necesitéis.
Instalando la librería
Recordemos que hoy en día tenemos dos formas distintas de instalar las librerías de Go, una sería seguir haciendo uso del go get
$ go get -u go.mongodb.org/mongo-driver
El cual si tenemos los modules activos además de instalarnos la librería la pondrá a disposición de nuestro proyecto en sus respectivos ficheros go.mod
y go.sum
O podemos simplemente importarla en nuestro proyecto y hacer uso del comando go mod tidy
el cual se encargará de añadirla a nuestro go.mod
y go.sum
e instalarla también.
Si te ves algo perdido con esto de los módulos te recomendamos nuestro artículo sobre como iniciarte en Go modules
Conectar con MongoDB
Antes de empezar con este punto quiero decir que todos los métodos de esta nueva librería de MongoDB esperaran un context
, hace poco hablamos sobre los contextos en Go, te será útil para manejar tus contextos dentro de la nueva librería de MongoDB para simplificar en este artículo, he decidido utilizar el context.TODO()
que si recordamos de dicho artículo se utiliza para indicar que esperamos un contexto pero aún no hemos decidido como acabar de utilizarlo.
Ahora sí veamos como conectarnos a un mongo que tenemos levantado en localhost
en el puerto por defecto 27017
.
package main
import (
"context"
"fmt"
"log"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func main() {
host := "localhost"
port := 27017
clientOpts := options.Client().ApplyURI(fmt.Sprintf("mongodb://%s:%d", host, port))
client, err := mongo.Connect(context.TODO(), clientOpts)
if err != nil {
log.Fatal(err)
}
// Check the connections
err = client.Ping(context.TODO(), nil)
if err != nil {
log.Fatal(err)
}
fmt.Println("Congratulations, you're already connected to MongoDB!")
}
Si vemos el código anterior, quizás nos sorprenda la línea, err = client.Ping(context.TODO(), nil)
, y es que aunque la función mongo.Connect
nos devuelva un client, nada nos asegura que realmente hayamos conectado correctamente, así que se nos ofrece un método Ping
el cual sino devuelve error es que todo ha ido correcto, así que con esta doble comprobación os ahorraréis futuros dolores de cabeza.
Recordemos que en Go no es necesario estar abriendo y cerrando conexiones contra nuestras bases de datos, como si que pasa en otros lenguajes de programación, ya que utilizaremos el pool de conexiones. Pero igualmente la librería nos ofrece una forma de realizar una desconexión de MongoDB si vemos que realmente ya no es necesario seguir utilizándolo, como podría ser el ejemplo de un script de un sólo uso, etc.
...
err = client.Disconnect(context.TODO())
if err != nil {
log.Fatal(err)
}
fmt.Println("Closed connection")
...
Modelando nuestro modelo
En Go creo que a nadie pilla por sorpresa si hablamos de los tags
, esos campos personalizables que sirven para realizar conversiones a otros tipos partiendo desde nuestro struct
, muy utilizado en el caso de json
por ejemplo. En el driver de MongoDB esto no iba a ser menos y tenemos su propio tag bson
, quizás si habéis utilizado la librería que mencionamos al principio del artículo esto no os coja de nuevo, ya que es completamente igual, así que dotando a nuestros parámetros del struct
de este tag, podremos personalizar el nombre de los campos que serán su representación en nuestro MongoDB, además habría que recalcar, que sino declaramos ningún tag, el valor por defecto del nombre del campo será el mismo que en la propiedad pero todo en minúscula.
type Starship struct {
ID string `bson:"-"`
Name string `bson:"name"`
Model string `bson:"model"`
CostInCredits int64 `bson:"costInCredits"`
Pilots []Pilot `bson:"pilots"`
}
Como véis es realmente sencillo crear nuestros modelos, además haciendo uso de bson:"-"
a la hora de persistir dicho modelo, ignorará esas propiedades, por ejemplo en el caso de nuestro ejemplo hemos decidido que el campo ID
sería util para Mysql
pero no nos es útil en MongoDB, así podemos tener un mismo modelo para ambos casos.
Operaciones CRUD
Ahora que ya podemos conectarnos a nuestra base de datos y además sabemos como crear nuestros modelos, es hora de realizar operaciones.
Recordemos que en MongoDB tenemos colecciones que vienen a ser similares a las tablas de Mysql que serán donde se almacenen nuestros documentos.
Antes de realizar ninguna operación tendremos que indicar al driver contra que colección queremos realizar la acción
collection := client.Database("swapi").Collection("starships")
Algo que hay que tener en cuenta con MongoDB y que tiene sus pros y contras, es que si intentamos insertar sobre una colección que no existe, la creará por defecto.
Insertando documentos
Esta claro que si queremos tener alguna información en nuestra base de datos lo primero que tenemos que hacer es realizar un insert
, vamos a ver como se hace:
deathStar := Starship{
Name: "Death Star",
Model: "DS-1 Orbital Battle Station",
CostInCredits: 1000000000000,
}
insertResult, err := collection.InsertOne(context.TODO(), deathStar)
if err != nil {
log.Fatal(err)
}
fmt.Println("Death Star had been inserted: ", insertResult.InsertID)
Como el propio nombre del método indica InsertOne
sólo podremos insertar un documento a la vez, ¿qué pasa si queremos insertar varios?
executor := Starship{
Name: "Executor",
Model: "Executor-class star dreadnought",
CostInCredits: 1143350000,
}
millenniumFalcon := Starship{
Name: "Millennium Falcon",
Model: "YT-1300 light freighter",
CostInCredits: 100000,
}
starships := []interface{}{executor, millenniumFalcon}
insertManyResult, err := collection.InsertMany(context.TODO(), starships)
if err != nil {
log.Fatal(err)
}
fmt.Println("Inserted multiple starships: ", insertManyResult.InsertedIDs)
El paquete bson
Anteriormente vimos que hacíamos uso de bson
para poder indicar que nombres serían los que tendrían nuestras propiedades dentro de los documentos de MongoDB, pues bien, también utilizaremos bson
para realizar filtrados u operaciones sobre MongoDB.
Para ello tendremos que importar el paquete go.mongodb.org/mongo-driver/bson
, este paquete nos ofrece cuatro tipos diferentes que podremos utilizar en nuestras operaciones.
D
: UnBSON document
. Este tipo se suele utilizar en situaciones que el orden importa, como los comandos de MongoDBM
: Viene a representar un mapa no ordenado, es igual que el tipo anterior, pero en este caso no tendrá un orden específico.A
: Un array deBSON
E
: Un elemento concreto dentro de un tipoD
Puede que aquí os estaréis preguntando, ¿qué es esto de BSON
? BSON
es la forma que tiene MongoDB de trabajar con los documentos, no es más que la representación de JSON en binario, (Binary-encoded JSON)
Modificando nuestros documentos
Ahora que ya conocemos un poco mejor como utilizar el paquete bson
podremos realizar operaciones de modificación sobre nuestros documentos, veamos como:
filter := bson.D{{"name", "Executor"}}
updateResult, err := collectionUpdateOne(context.TODO(), filter, bson.D{
{"$set", bson.D{
{"name", "Executor Black"},
}},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Matched %v documents and updated %v documents.\n", updateResult.MatchedCount, updateResult.ModifiedCount)
Buscando documentos
Ya va siendo hora de comprobar si realmente nuestras acciones han dado resultado y se han ejecutado correctamente, así que vamos a buscar si nuestras naves han sido realmente persistidas en la base de datos.
Para poder buscar documentos, tendremos que hacer uso de un filtro y este será utilizado con bson
al igual que hemos visto en los updates, así que aprovecharemos el filtro anterior filter := bson.D{{"name", "Executor"}}
var result Starship
err = collection.FindOne(context.TODO(), filter).Decode(&result)
if err !=nil {
log.Fatal(err)
}
fmt.Printf("Starship found: %v\n", result)
Realmente este ejemplo es un poco mentirijilla, ya que si no hay resultado no devolverá error sino que dejará nuestra estructura a su valor inicial, así que tendríamos que asegurarnos que realmente nuestro struct
ha sido inicializado:
if result.Name != "" {
fmt.Printf("Starship found: %v\n", result)
} else {
fmt.Println("Starship not found")
}
Bueno, ya vemos que buscar un sólo documento resulta bastante sencillo, vamos a ver ahora como resolver la búsqueda de más de un documento a la vez:
findOpts := options.Find()
findOpts.SetLimit(2)
var results []*Starship
cur, err := collection.Find(context.TODO(), bson.D{{}}, findOptions)
if err != nil {
log.Fatal(err)
}
for cur.Next(context.TODO()) {
// create a value into which the single document can be decoded
var s Starship
err := cur.Decode(&s)
if err != nil {
log.Fatal(err)
}
results = append(results, &s)
}
if err := cur.Err(); err != nil {
log.Fatal(err)
}
cur.Close(context.TODO())
fmt.Printf("Found multiple documents (array of pointers): %+v\n", results)
Aquí se nos complica un poco la cosa, pero vayamos por parte, lo primero que vemos es que hemos declarado unas opciones de búsquedas, findOpts.SetLimit(2)
y es que si queremos ordenar, hacer un limit, u otro tipo de operaciones tendremos que hacer uso del paquete go.mongodb.org/mongo-driver/mongo/options
, os recomendamos visitar la documentación del propio paquete para averiguar todo lo que puedes hacer con él.
Por otro lado, vemos que pasamos un bson.D{{}}
al método Find
esto es porque no queremos realizar ningún filtro en particular, sino lo haríamos como lo hemos visto en casos anteriores. El método Find
nos devolverá un cursor
el cual tendremos que recorrernos para poder añadir los resultados a nuestro slice
.
Eliminando documentos
Por último pero no menos importante tenemos la opción de eliminar documentos. Como hemos visto en otras operaciones tenemos la opción de borrar un sólo registro pasando un filtro, collection.DeleteOne()
o borrar varios de golpe, con un filtro o a lo loco sin where
collection.DeleteMany()
, e incluso tenemos collection.Drop()
para eliminar todos los documentos de una colección.
deleteResult, err := collection.DeleteOne(context.TODO(), filter)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Starship deleted", deleteResult.DeletedCount)
Próximos pasos
Si quieres ver como ha quedado todo el código que hemos ido creando a lo largo del artículo, podéis verlo en el siguiente Gist. Os recomendamos encarecidamente que os ayudéis de la increíble documentación que tiene la propia librería haciendo uso de GoDoc.
Además si quieres colaborar con el proyecto, podéis reportar bugs que encontréis en el JIRA de MongoDB o también podéis hacer uso de su Google Group para dudas y sugerencias.
Obviamente nosotros siempre estaremos encantados de resolver vuestras dudas que dejéis en los comentarios o a través de nuestra cuenta de Twitter @FriendsofGoTech