Here is an example how to use lungo.
We will try to build a couple of endpoints:
- Get Category with populated items inside
- Get Item by ID with populated category inside
Overall project structure will look like this:
- example
- - main.go
- - models
- - - Category.go
- - - Item.go
- - handlers
- - - GetCategoryById.go
- - - GetItemById.go
- - go.mod
- - go.sum
Let’s create directory for our example and initialize the Go module:
mkdir example
cd example
go mod init golungo/example
Connect
Let’s start with connection. Create main.go file and put the following code.
package main
import (
"os"
"github.com/golungo/lungo"
)
func main() {
uri := os.GetEnv("MONGO_URI")
if err := lungo.Init(uri); err != nil {
panic(err)
}
if err := lungo.Connect(); err != nil {
panic(err)
}
defer func() {
if err := lungo.Disconnect(); err != nil {
panic(err)
}
}()
}
Define models
Since I want to show you how to populate data across the models, let’s create two models:
- Category Model
- Item Model
Each model will “inherit” the lungo/query Model, to provide Methods like Match, Lookup, Virtual and Exec to our model, and it must remain “invisible” to json and bson packages. As well, each model must provide a “GetModel” method, where it will be Initialized and configured before each request to mongodb.
Category Model
Our category will contain only two actual properties and one virtual property:
- ID - ObjectID of a document
- Title - Category title
- Items - A “Virtual” property, where we will be populating our items.
package models
import (
"reflect"
"github.com/golungo/lungo"
"github.com/golungo/lungo/query"
)
type Category struct {
query.Model `json:"-" bson:"-"`
ID lungo.ObjectID `json:"_id" bson:"_id"`
Items []Item `json:"items,omitempty" bson:"-"`
}
func GetCategoryCollection() query.Model {
var model Category
return model.Init(
"categories", reflect.TypeOf(model),
).Virtual(
"items", "_id", "categoryId", "items", false,
)
}
Item Model
Items will contain four actual properties and one virtual as well:
- ID - ObjectID of a mongo document
- Title - Item title
- Content - item content
- CategoryID - ID of item category, which we will be using to join with categories collection
- Category - A “Virtual” property, where we will be populating the item category data
package models
import (
"reflect"
"github.com/golungo/lungo"
"github.com/golungo/lungo/query"
)
type Item struct {
query.Model `json:"-" bson:"-"`
ID lungo.ObjectID `json:"_id" bson:"_id"`
Title string `json:"title" bson:"title"`
Content string `json:"content" bson:"content"`
CategoryID lungo.ObjectID `json:"categoryId" bson:"categoryId"`
Category []Category `json:"category" bson:"-"`
}
func GetItemCollection() query.Model {
var model Item
return model.Init(
"items", reflect.TypeOf(model),
).Virtual(
"categories", "categoryId", "_id", "category", false,
)
}
Let’s talk about “Virtual” properties. The main purpose of a virtual property is to provide a way to work with the result of mongodb $lookup operation.
The .Virtual()
method takes as an arguments:
- The target collection name, to search documents from
- The local field name, to join with, defined in ‘bson’ struct tag
- The foreign field name, to join with, defined in ‘bson’ struct tag
- The name of a virtual field, to access the result, also defined in ‘bson’ struct tag
After that, using lungo.Fields
type, we can provide the information which fields we want to lookup and populate.
Controllers
We will create two controllers, for data fetching of Categories and Items.
Let’s write the GetCategoryById()
controller first.
Get Category By Id
This method will take as an argument the seeking Category ID with a type of lungo.ObjectID
, which is basically the proxied version of primitive.ObjectID
.
package controllers
import (
"encoding/json"
"golungo/example/models"
"github.com/golungo/lungo"
)
func GetCategoryById(categoryId lungo.ObjectID) (models.Category, error) {
var Categories []models.Category
filter := lungo.Filter{
"_id": categoryId
}
populate := lungo.Fields{
"items"
}
result, err := models.GetCategoryCollection().Match(filter).Lookup(populate).Exec()
if err != nil {
return Categories, err
}
if err := json.Unmarshal(result, &Categories); err != nil {
return Categories, err
}
if len(Categories) == 0 {
return Categories, err
}
return Categories[0], nil
}
Get Item By Id
Same way as the previous one, this controller will take an argument of itemId with type lungo.ObjectID
.
As you will see next, it is basically the same method, but for Item model.
package controllers
import (
"encoding/json"
"golungo/example/models"
"github.com/golungo/lungo"
)
func GetItemById(itemId lungo.ObjectID) (models.Item, error) {
var Items []models.Item
filter := lungo.Filter{
"_id": itemId
}
populate := lungo.Fields{
"category"
}
result, err := models.GetItemsCollection().Match(filter).Lookup(populate).Exec()
if err != nil {
return Items, err
}
if err := json.Unmarshal(result, &Items); err != nil {
return Items, err
}
if len(Categories) == 0 {
return Items, err
}
return Items[0], nil
}
Route Handlers
For this example i will use the Gin Web Framework to make it simple.
Let’s write the Get Category by id route handler, responding to /api/v1/categories/:categoryId
and the Get Item By Id route handler, responding to /api/v1/items/:itemId
.
They will be basically the same methods :)
Get Category by Id
package handlers
import (
"golungo/example/controllers"
"github.com/gin-gonic/gin"
"github.com/golungo/lungo"
)
func GetCategoryById(c *gin.Context) {
categoryId, err := lungo.ObjectIDFromHex(c.Param("categoryId"))
if err != nil {
c.JSON(404, "Not Found")
c.Abort()
return
}
Category, err := controllers.GetCategoryById(categoryId)
if err != nil {
c.JSON(404, "Not Found")
c.Abort()
return
}
c.JSON(200, Category)
c.Abort()
}
Get Item by Id
package handlers
import (
"golungo/example/controllers"
"github.com/gin-gonic/gin"
"github.com/golungo/lungo"
)
func GetItemById(c *gin.Context) {
itemId, err := lungo.ObjectIDFromHex(c.Param("itemId"))
if err != nil {
c.JSON(404, "Not Found")
c.Abort()
return
}
Item, err := controllers.GetItemById(itemId)
if err != nil {
c.JSON(404, "Not Found")
c.Abort()
return
}
c.JSON(200, Item)
c.Abort()
}
Wrapping it all up
Now, when we have our models, controllers and handlers - we can wrap it all up in main.go
file and run it!
Our final main.go file will look like this:
package main
import (
"os"
"golungo/example/handlers"
"github.com/golungo/lungo"
"github.com/gin-gonic/gin"
)
func main() {
uri := os.GetEnv("MONGO_URI")
if err := lungo.Init(uri); err != nil {
panic(err)
}
if err := lungo.Connect(); err != nil {
panic(err)
}
defer func() {
if err := lungo.Disconnect(); err != nil {
panic(err)
}
}()
router := gin.Default()
router.GET("/api/v1/categories/:categoryId", handlers.GetCategoryById)
router.GET("/api/v1/items/:itemId", handlers.GetItemById)
router.Run(":8080")
}
Now, when you will make a GET request to http://localhost:8080/api/v1/categories/:categoryId
, you will receive the category with all the items, inside it.
And same for http://localhost:8080/api/v1/items/:itemId
- there you will see populated category.
Example Repository and playground
I made an example repository with this small project, populated with data and configured MongoDB so you can fork it and play around.
https://github.com/golungo/example